Start
Getting started
Install#
pnpm add @ogdakke/fluidboxnpm install @ogdakke/fluidboxyarn add @ogdakke/fluidboxbun add @ogdakke/fluidboxThe framework bindings are in the same package. React, Vue, Solid and Angular are optional peer dependencies, so install only the one your app already uses.
Import the styles#
The viewer's layout and default look are in one stylesheet. Import it once, in a client entry, before your own CSS:
import "@ogdakke/fluidbox/styles.css";The filmstrip and sheet add-ons have their own stylesheets, @ogdakke/fluidbox/filmstrip.css and @ogdakke/fluidbox/sheet.css. Import each only if you use that add-on.
Describe your items#
An item is a plain object whose fields are the attributes of the element the viewer draws: src, srcset, sizes and alt for an image, as on <img>. It needs width and height, so the viewer can size the slide before the file loads. A thumbnail gives the viewer a picture to show while the full image loads.
import type { FluidboxItem } from "@ogdakke/fluidbox";
export const photos: FluidboxItem[] = [
{
type: "img",
src: "/photos/aurora.jpg",
alt: "Aurora over the tundra",
width: 2200,
height: 3300,
thumbnail: { src: "/photos/aurora-thumb.jpg", width: 440, height: 660 },
},
];Each item is known by its id, which defaults to its src. Give items an id of your own when two share a src, or when you want to open one by a name. See Media types for video, iframes and custom content.
A first gallery#
One viewer for the whole list, and a trigger for each thumbnail. Pick your framework in the tabs:
Loading the live example…
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/react";
import { photos } from "./data";
export function Basic() {
return (
<Fluidbox.Root items={photos}>
<div className="pattern-grid">
{photos.map((photo) => (
<Fluidbox.Trigger
key={photo.src}
item={photo}
className="pattern-thumb"
>
<img src={photo.src} alt={itemLabel(photo)} />
</Fluidbox.Trigger>
))}
</div>
{/* Without a Viewer, the viewer has these same three buttons. */}
<Fluidbox.Viewer>
<Fluidbox.Previous />
<Fluidbox.Next />
<Fluidbox.Close />
</Fluidbox.Viewer>
</Fluidbox.Root>
);
}<script setup lang="ts">
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/vue";
import { photos } from "./data";
</script>
<template>
<!-- Without a Viewer, the viewer has the default Previous, Next and Close. -->
<Fluidbox.Root :items="photos">
<div class="pattern-grid">
<Fluidbox.Trigger
v-for="photo in photos"
:key="photo.id"
:item="photo"
class="pattern-thumb"
>
<img :src="photo.src" :alt="itemLabel(photo)" />
</Fluidbox.Trigger>
</div>
</Fluidbox.Root>
</template>import { For } from "solid-js";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/solid";
import { photos } from "./data";
export function Basic() {
return (
<Fluidbox.Root items={photos}>
<div class="pattern-grid">
<For each={photos}>
{(photo) => (
<Fluidbox.Trigger item={photo} class="pattern-thumb">
<img src={photo.src} alt={itemLabel(photo)} />
</Fluidbox.Trigger>
)}
</For>
</div>
{/* Without a Viewer, the viewer has these three by default. */}
<Fluidbox.Viewer>
<Fluidbox.Previous />
<Fluidbox.Next />
<Fluidbox.Close />
</Fluidbox.Viewer>
</Fluidbox.Root>
);
}import { Component } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import { FluidboxModule } from "@ogdakke/fluidbox/angular";
import { photos } from "./data";
@Component({
selector: "pattern-basic",
standalone: true,
imports: [FluidboxModule],
template: `
<div fluidboxRoot [items]="photos" class="pattern-grid">
@for (photo of photos; track photo.id) {
<button [fluidboxTrigger]="photo" class="pattern-thumb">
<img [src]="photo.src" [alt]="label(photo)" />
</button>
}
<!-- Without a viewer template these three are the default; with one, you place them. -->
<ng-template fluidboxViewer>
<button fluidboxPrevious></button>
<button fluidboxNext></button>
<button fluidboxClose></button>
</ng-template>
</div>
`,
})
export class Basic {
readonly photos = photos;
readonly label = itemLabel;
}<fluidbox-gallery class="pattern-grid">
<fluidbox-item item-id="photo-aurora">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/aurora.svg" alt="Aurora over the tundra" />
</button>
<template slot="content">
<img src="/art/aurora.svg" alt="Aurora over the tundra" width="1100" height="1650" />
</template>
</fluidbox-item>
<fluidbox-item item-id="photo-ridge">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/ridge.svg" alt="Night ridge" />
</button>
<template slot="content">
<img src="/art/ridge.svg" alt="Night ridge" width="1200" height="1500" />
</template>
</fluidbox-item>
<fluidbox-item item-id="photo-fjord">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/fjord.svg" alt="Fjord in the blue hour" />
</button>
<template slot="content">
<img src="/art/fjord.svg" alt="Fjord in the blue hour" width="1200" height="1600" />
</template>
</fluidbox-item>
<fluidbox-item item-id="photo-lagoon">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/lagoon.svg" alt="Lagoon after sunset" />
</button>
<template slot="content">
<img src="/art/lagoon.svg" alt="Lagoon after sunset" width="1800" height="1200" />
</template>
</fluidbox-item>
</fluidbox-gallery>import { createFluidbox, itemLabel } from "@ogdakke/fluidbox";
import { photos } from "./data";
export function mountBasic(root: HTMLElement) {
// The viewer has Previous, Next and Close buttons unless you draw your own.
const fluidbox = createFluidbox({ items: photos });
const grid = document.createElement("div");
grid.className = "pattern-grid";
photos.forEach((photo, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = "pattern-thumb";
button.append(Object.assign(new Image(), { src: photo.src, alt: itemLabel(photo) }));
grid.append(button);
fluidbox.bind(button, index);
});
root.append(grid);
return () => {
// Also unbinds the buttons.
fluidbox.destroy();
grid.remove();
};
}The examples import photos from a local ./data module, described in the example data. They import itemLabel from @ogdakke/fluidbox, which returns an item's alt or title. Every API shows the default Previous, Next and Close buttons until you give the viewer content of your own. After that, the controls option, the Controls part or <fluidbox-controls> keeps them.
Framework setup#
| Framework | Import | Peer dependency |
|---|---|---|
| React | @ogdakke/fluidbox/react |
react and react-dom 18 or later |
| Vue | @ogdakke/fluidbox/vue |
vue 3.3 or later |
| Solid | @ogdakke/fluidbox/solid |
solid-js 1.8 or later |
| Angular | @ogdakke/fluidbox/angular |
@angular/core 17 or later |
| Custom elements | @ogdakke/fluidbox/elements |
none |
| Plain JavaScript | @ogdakke/fluidbox |
none |
Every binding has the same parts with the same names: Root holds the items and owns the viewer, Trigger opens an item, Viewer renders into the viewer's layer, and Filmstrip and Sheet turn those add-ons on by being there. useFluidbox() (injectFluidbox() in Angular) reads the state and returns the commands. Each framework page shows them in its own idiom: React, Vue, Solid, Angular. Next.js covers the React binding with server components and next/image.
- Custom elements.
<fluidbox-gallery>with<fluidbox-item>children. See Custom elements. - Plain JavaScript.
createFluidbox(), andfluidbox.bind()for each trigger. See JavaScript.
Loading the viewer#
The page loads the triggers, the items and the viewer's state. The viewer itself, with its gestures, transitions and slides, is a chunk of its own. By default it loads when a mouse rests on a trigger for 100 ms, a finger or pen touches one, or one takes focus, so it has usually arrived by the time the click does. An open loads it if it hasn't.
The loading option picks the moment:
loading |
The viewer's code loads |
|---|---|
"hover" (default) |
When a mouse rests on a trigger for 100 ms, a finger or pen touches one, or one takes focus. |
"press" |
When a trigger is pressed. Nothing loads for a pointer that only passes over. |
"eager" |
Once the page is idle, whether or not anyone opens a viewer. |
For a signal of your own, call load(). A route that's about to show the gallery, or a menu that leads to it, can start the download early:
menu.addEventListener("toggle", () => void fluidbox.load(), { once: true });load() resolves true once the code has loaded and false if it failed, as when offline. A failed load tries again on the next call or open. The bindings take loading on Root and put load next to the commands, and <fluidbox-gallery> has a loading attribute and a load() method.
Add-ons#
The filmstrip and the sheet are add-ons with their own entries, so their code loads only when you use them. Turn one on, with an option, a framework part or a <fluidbox-filmstrip> or <fluidbox-sheet> child, and the fluidbox loads its entry. An open waits for it. To bundle an entry up front, import it once in the browser bundle:
import "@ogdakke/fluidbox/filmstrip";
import "@ogdakke/fluidbox/filmstrip.css";
import "@ogdakke/fluidbox/sheet";
import "@ogdakke/fluidbox/sheet.css";Each needs its stylesheet, filmstrip.css or sheet.css, whichever way you turn it on. See Filmstrip and sheet.
Server rendering#
The package root, @ogdakke/fluidbox, is safe to import on the server. It has no side effects and touches no browser globals until you call createFluidbox(). The add-on entries are safe there too. The framework bindings create the viewer only in the browser, after mount, so they render on the server as plain markup. Next.js walks through one server-rendered setup end to end.
Importing @ogdakke/fluidbox/elements defines the custom elements, so that entry is browser only. In a server-rendered app, import it after hydration:
if (typeof customElements !== "undefined") await import("@ogdakke/fluidbox/elements");Next#
- The patterns show nine common setups in every framework.
- Items and origins covers large or virtualized lists, and where the flight lands.
- Styling shows how to theme the viewer and add your own controls.