Frameworks
Custom elements
@ogdakke/fluidbox/elements registers <fluidbox-gallery>, <fluidbox-item>, <fluidbox-filmstrip> and <fluidbox-sheet>. You write the gallery as markup. The gallery reads it into items and owns one fluidbox for them. The elements need no framework, and work inside any framework that renders plain HTML.
Install#
pnpm add @ogdakke/fluidboxnpm install @ogdakke/fluidboxyarn add @ogdakke/fluidboxbun add @ogdakke/fluidboximport "@ogdakke/fluidbox/elements";
import "@ogdakke/fluidbox/styles.css";@ogdakke/fluidbox/elements defines the elements when it loads, so import it only in the browser. On a server-rendered page, import it after hydration:
if (typeof customElements !== "undefined") await import("@ogdakke/fluidbox/elements");A gallery#
<fluidbox-gallery>
<fluidbox-item item-id="dune">
<button slot="trigger" type="button">
<img src="/photos/dune-400.jpg" alt="Dune at dusk" />
</button>
<template slot="content">
<img src="/photos/dune.jpg" alt="Dune at dusk" width="1800" height="1200" />
</template>
</fluidbox-item>
<fluidbox-item item-id="fjord">
<button slot="trigger" type="button">
<img src="/photos/fjord-400.jpg" alt="Fjord" />
</button>
<template slot="content">
<img src="/photos/fjord.jpg" alt="Fjord" width="1200" height="1600" />
</template>
</fluidbox-item>
</fluidbox-gallery>The gallery adds Previous, Next and Close buttons to the viewer unless you give it viewer content of your own. The same gallery, live, in every framework:
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();
};
}Elements#
| Element | Class | Role |
|---|---|---|
<fluidbox-gallery> |
FluidboxGalleryElement |
Owns one fluidbox for its <fluidbox-item> children, or for the items you set. |
<fluidbox-item> |
FluidboxItemElement |
One item: its trigger and its content. A direct child of the gallery. |
<fluidbox-filmstrip> |
FluidboxFilmstripElement |
Hidden. As a direct child of the gallery, it turns the filmstrip on. |
<fluidbox-sheet> |
FluidboxSheetElement |
Hidden. As a direct child of the gallery, it turns the sheet on and holds its content. |
<fluidbox-controls> |
none | Inside <template slot="viewer">, stands for the default Previous, Next and Close buttons. |
@ogdakke/fluidbox/elements exports the four classes. Importing it adds the tags to HTMLElementTagNameMap, so querySelector("fluidbox-gallery") is typed, and fluidbox-preview-change to HTMLElementEventMap.
<fluidbox-item>#
| Attribute | What it does |
|---|---|
item-id |
Required, unique in the gallery. The item's id for commands, events and returns. |
returns |
Space-separated ids of more items whose close flight lands on this trigger, as for an album cover. |
placeholder |
An image painted behind an image slide until it decodes, such as a tiny data URI. |
aria-label |
The title of custom content (see below). Defaults to item-id. |
| Child | What it does |
|---|---|
[slot="trigger"] |
The element that opens the item, usually a <button>. Its first <img> becomes the item's thumbnail. Leave it out for an item with no thumbnail of its own, such as the rest of an album. |
<template slot="content"> |
Required. One root element: the media (see below). Inert until the viewer shows it. |
<template slot="sheet"> |
This item's sheet content, when the gallery has a <fluidbox-sheet>. |
What the content template becomes#
The template's root element becomes a flat item, attribute for attribute:
| Root element of the template | Item type | Read from it |
|---|---|---|
<img>, or <picture> with an <img> |
img |
src, srcset, sizes, alt, and width and height (required). Each <source> of a <picture> becomes one of sources. |
<video> |
video |
src, width and height (required), poster, autoplay, loop, muted, playsinline, controls, preload, and aria-label or title. |
<iframe src> |
iframe |
src, width and height (required), title, allow, referrerpolicy and sandbox. |
| anything else | custom |
The template is cloned into the slide. width and height on the root set its size; without them, the trigger image's size. |
The gallery reads the boolean attributes of a <video> as HTML does, so an attribute that is present is on. Write controls and playsinline on it, as you would on the page. See Media types for what each type does in the viewer.
| Property | Type | What it is |
|---|---|---|
itemId |
string |
The item-id. Throws when it is empty. |
returns |
readonly string[] |
The ids in returns. |
trigger |
HTMLElement | null |
The slot="trigger" child. |
| Event | detail |
When |
|---|---|---|
fluidbox-preview-change |
{ id: string; item: FluidboxItem } |
Before a close flight lands on this trigger, with the item it lands on: its own or one in returns. Bubbles. Show that item on the trigger here, synchronously. |
<fluidbox-gallery>#
| Attribute | What it does |
|---|---|
nozoom |
Turns zoom off. |
zoom-step |
The scale of a double-tap or double-click zoom. Default 2. |
zoom-max |
The largest zoom scale. Default 4. |
loop |
Go round: past the last item comes the first, by swipe, key, button and filmstrip. A single item never loops. |
loading |
When the viewer's code loads: hover (the default), press or eager. The loading property reflects it. See Loading the viewer. |
open |
Opens the viewer on index as it is added, and closes it as it is removed. Leave it off to let the triggers own the viewer. |
index |
The item the viewer shows while open is set, as an index. Changing it moves the open viewer. An index out of range is ignored. |
The properties are the createFluidbox options the markup can't say, and the fluidbox's state and handles:
| Property | Type | What it is |
|---|---|---|
items |
FluidboxItems | null |
Items to use instead of the <fluidbox-item> children: an array or a lazy list (see Items and origins). |
renderers |
Record<string, FluidboxRenderer> |
Draw custom items of items, by the name in their render. Read when a slide mounts. |
viewer |
FluidboxViewerRenderer | undefined |
Your own viewer content. Wins over <template slot="viewer"> and the default controls. |
origins |
FluidboxOriginAdapter | undefined |
Origins for items with no trigger in the gallery, and revealOrigin to scroll one into view. |
zoom |
false | { step?, max? } | undefined |
Zoom options, or false. Wins over nozoom and the zoom attributes. |
labels |
Partial<FluidboxLabels> | undefined |
Your strings for the viewer. See Labels. |
state |
FluidboxState |
The viewer's state, or a closed state before the gallery is ready. |
sheet |
FluidboxSheet | null |
The sheet, while a <fluidbox-sheet> turns it on. |
fluidbox |
Fluidbox | null |
The fluidbox behind the gallery, or null before it is ready. |
| Method | Returns | What it does |
|---|---|---|
open(target?, options?) |
Promise<boolean> |
Opens the viewer on an index or id, or moves to it while open. |
close(options?) |
Promise<boolean> |
Closes the viewer. |
next(options?) |
Promise<boolean> |
Next item. |
previous(options?) |
Promise<boolean> |
Previous item. |
setItems(items) |
Promise<boolean> |
Replaces the items, as the items property does, and resolves once they apply. |
preload(target) |
() => void |
Starts loading an item a press is likely to open. Call the result to let go. |
load() |
Promise<boolean> |
Loads the viewer's code now, once the gallery has read its items. |
subscribe(listener) |
() => void |
Calls listener(state) on every state change. |
whenReady() |
Promise<Fluidbox> |
Resolves once the gallery has read its items. Rejects if the gallery is not connected. |
The commands work as the fluidbox's do, with an item, an index or an id as the target. Each waits until the gallery has read its items, and rejects if the gallery isn't connected.
Events#
The gallery fires the fluidbox's events: beforetoggle, toggle, beforeitemchange, itemchange, settle and sheetchange, with the same fields (see Events). They don't bubble, like a popover's. Canceling beforetoggle or beforeitemchange on the gallery cancels the command.
const gallery = document.querySelector("fluidbox-gallery")!;
gallery.addEventListener("itemchange", (event) => console.log("now showing", event.id));
gallery.subscribe((state) => console.log(state.status, state.index));
await gallery.open("fjord");The listeners are typed through the gallery's event map. @ogdakke/fluidbox exports the event classes, FluidboxItemChangeEvent, FluidboxSheetEvent and FluidboxToggleEvent.
Markers#
| Attribute | Put it on | Effect |
|---|---|---|
data-fluidbox-origin |
an element inside a trigger | The flight starts and ends on this element instead of the trigger's image. |
data-fluidbox-thumbnail-overlay |
an element inside a trigger | A thumbnail overlay, one per trigger. It stays on the page and gets --fluidbox-closed. ="travel" makes it fly with the photo. |
data-fluidbox-frame-layer |
a travelling overlay, or an element in it | Moves into the flying frame for the flight and scales with it, as a scrim does. |
data-fluidbox-item-id="id" |
any element in a gallery with items set |
Makes the element a trigger for that item. |
data-fluidbox-close, data-fluidbox-previous, data-fluidbox-next |
a button in the viewer content | Runs that command, with the default control styling. Previous and Next are disabled at the ends, Close while closed. |
data-fluidbox-sheet-stop="N" |
a button in the viewer | Moves the sheet to stop N. |
Each trigger also gets data-fluidbox-trigger, aria-haspopup="dialog" and aria-expanded. The trigger's default styles use :where(), so your own classes win.
Viewer content#
The gallery fills the viewer's layer with the first of these that exists:
- the
viewerproperty, a renderer called with{ target, fluidbox, signal }, as thevieweroption; - a
<template slot="viewer">that is a direct child of the gallery, cloned into the viewer, where<fluidbox-controls>expands to the default buttons; - the default Previous, Next and Close buttons.
<fluidbox-gallery>
<!-- items -->
<template slot="viewer">
<fluidbox-controls></fluidbox-controls>
<button type="button" data-fluidbox-sheet-stop="1">Details</button>
</template>
</fluidbox-gallery>Filmstrip and sheet#
A <fluidbox-filmstrip> child turns the filmstrip on, and a <fluidbox-sheet> child the sheet. The gallery loads their add-on code itself, so you don't import @ogdakke/fluidbox/filmstrip or /sheet. Each still needs its stylesheet, @ogdakke/fluidbox/filmstrip.css or @ogdakke/fluidbox/sheet.css.
<fluidbox-sheet> attribute |
What it does |
|---|---|
snap-points |
The stops above rest, lowest first: "40% 90%", pixels such as "240", or content. |
dismiss |
flight, scroll or none. See Stops. |
The sheet shows the active item's <template slot="sheet">, else the <template> inside <fluidbox-sheet>. Set the sheet element's render property to draw it yourself, and the filmstrip element's render to fill each thumbnail button. Both are renderers that get { item, index, target, signal }.
const strip = document.querySelector("fluidbox-filmstrip")!;
strip.render = ({ index, target }) => {
target.textContent = String(index + 1);
};Patterns#
| Pattern | With custom elements |
|---|---|
| Gallery | A <fluidbox-item> per photo. The default controls come with it. |
| One trigger for an album | returns="..." on the cover item, items without a trigger, fluidbox-preview-change, and a travelling overlay. |
| Rich origins | data-fluidbox-origin on an element inside the trigger. |
| Custom content | Any element in the content template, or gallery.items with gallery.renderers. |
| Programmatic control | gallery.open(2), gallery.subscribe(), and beforeitemchange with preventDefault(). |
| Zoom | zoom-step, zoom-max and nozoom, or the zoom property. |
| Filmstrip | A <fluidbox-filmstrip> child, with a render of your own. |
| Sheet | A <fluidbox-sheet snap-points> child, and a data-fluidbox-sheet-stop button in <template slot="viewer">. |
| Headless controls | gallery.viewer, or <template slot="viewer">. |
| Right to left | A dir="rtl" ancestor, and the labels property. |
| Photo in the address | The open and index attributes, set from ?photo= and from toggle and itemchange. |
Notes#
Markup changes are picked up. The gallery watches its children. Adding or removing items, or changing an item's item-id, returns, placeholder, aria-label or slot, re-reads the items on the next microtask. An open viewer stays on the same item id if it is still there. The content inside a trigger is yours. Swapping its picture, as an album cover does, changes nothing in the gallery. The gallery reads the trigger's thumbnail when it reads the item, and reads a <template slot="content"> when it re-reads the items.
Options change live. Setting viewer, zoom or labels, changing nozoom, zoom-step, zoom-max or loading, adding or removing a <fluidbox-filmstrip> or <fluidbox-sheet>, and changing the sheet's attributes or either element's render update the viewer through update, without closing it. The gallery keeps one fluidbox for as long as it is connected.
Focus. After a close whose trigger is gone, the gallery focuses itself, so focus doesn't fall to the page. For that it sets tabindex="-1" on itself, unless it already has one.
Removing the gallery from the document destroys its viewer.