Patterns
Programmatic control
The fluidbox opens, moves and closes the viewer from code. Its state tells you what is open, and events tell you what changed and why.
Loading the live example…
programmatic.tsx
import { useRef, useState, type RefObject } from "react";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidbox } from "@ogdakke/fluidbox/react";
import { photos } from "./data";
function Toolbar({ lock }: { lock: RefObject<HTMLInputElement | null> }) {
const open = useFluidbox(({ open }) => open);
const status = useFluidbox(({ status, index }) => `${status} ${index ?? "-"}`);
return (
<div className="pattern-toolbar">
<button type="button" onClick={() => open(2)}>
Open the third photo
</button>
<label>
<input ref={lock} type="checkbox" name="lock" /> Lock navigation
</label>
<output>{status}</output>
</div>
);
}
export function Programmatic() {
const lock = useRef<HTMLInputElement>(null);
const [log, setLog] = useState<string[]>([]);
const record = (event: Event) => setLog((types) => [...types, event.type].slice(-20));
return (
<Fluidbox.Root
items={photos}
// beforeitemchange comes before every move between items; canceling it keeps the viewer still.
onBeforeItemChange={(event) => {
if (lock.current?.checked) event.preventDefault();
record(event);
}}
onBeforeToggle={record}
onToggle={record}
onItemChange={record}
onSettle={record}
>
<Toolbar lock={lock} />
<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>
<ol className="pattern-log">
{log.map((type, index) => (
<li key={index}>{type}</li>
))}
</ol>
</Fluidbox.Root>
);
}Programmatic.vue
<script setup lang="ts">
import { ref } from "vue";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, type FluidboxItemChangeEvent } from "@ogdakke/fluidbox/vue";
import { photos } from "./data";
const locked = ref(false);
const log = ref<string[]>([]);
const record = (event: Event) => {
log.value = [...log.value, event.type].slice(-20);
};
// beforeitemchange comes before every move between items; canceling it keeps the viewer still.
function onBeforeItemChange(event: FluidboxItemChangeEvent) {
if (locked.value) event.preventDefault();
record(event);
}
</script>
<template>
<Fluidbox.Root
v-slot="{ status, index, open }"
:items="photos"
@beforetoggle="record"
@toggle="record"
@beforeitemchange="onBeforeItemChange"
@itemchange="record"
@settle="record"
>
<div class="pattern-toolbar">
<button type="button" @click="open(2)">Open the third photo</button>
<label>
<input v-model="locked" type="checkbox" name="lock" /> Lock navigation
</label>
<output>{{ status }} {{ index ?? "-" }}</output>
</div>
<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>
<ol class="pattern-log">
<li v-for="(type, entry) in log" :key="entry">{{ type }}</li>
</ol>
</Fluidbox.Root>
</template>programmatic.tsx
import { createSignal, For } from "solid-js";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidbox } from "@ogdakke/fluidbox/solid";
import { photos } from "./data";
function Toolbar(props: { locked: boolean; onLockedChange: (locked: boolean) => void }) {
const fluidbox = useFluidbox();
return (
<div class="pattern-toolbar">
<button type="button" onClick={() => fluidbox.open(2)}>
Open the third photo
</button>
<label>
<input
type="checkbox"
name="lock"
checked={props.locked}
onChange={(event) => props.onLockedChange(event.currentTarget.checked)}
/>{" "}
Lock navigation
</label>
<output>
{fluidbox.status} {fluidbox.index ?? "-"}
</output>
</div>
);
}
export function Programmatic() {
const [locked, setLocked] = createSignal(false);
const [log, setLog] = createSignal<string[]>([]);
const record = (event: Event) => setLog((types) => [...types, event.type].slice(-20));
return (
<Fluidbox.Root
items={photos}
// beforeitemchange comes before every move between items; canceling it keeps the viewer still.
onBeforeItemChange={(event) => {
record(event);
if (locked()) event.preventDefault();
}}
onBeforeToggle={record}
onToggle={record}
onItemChange={record}
onSettle={record}
>
<Toolbar locked={locked()} onLockedChange={setLocked} />
<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>
<ol class="pattern-log">
<For each={log()}>{(type) => <li>{type}</li>}</For>
</ol>
</Fluidbox.Root>
);
}programmatic.ts
import { Component, signal } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import { FluidboxModule, type FluidboxItemChangeEvent } from "@ogdakke/fluidbox/angular";
import { photos } from "./data";
@Component({
selector: "pattern-programmatic",
standalone: true,
imports: [FluidboxModule],
template: `
<div class="pattern-toolbar">
<button type="button" (click)="fb.open(2)">
Open the third photo
</button>
<label>
<input
type="checkbox"
name="lock"
#lock
(change)="locked.set(lock.checked)"
/>
Lock navigation
</label>
<output>{{ fb.state().status }} {{ fb.state().index ?? "-" }}</output>
</div>
<div
fluidboxRoot
#fb="fluidbox"
[items]="photos"
class="pattern-grid"
(beforeToggle)="log($event)"
(toggle)="log($event)"
(beforeItemChange)="guard($event)"
(itemChange)="log($event)"
(settle)="log($event)"
>
@for (photo of photos; track photo.id) {
<button [fluidboxTrigger]="photo" class="pattern-thumb">
<img [src]="photo.src" [alt]="label(photo)" />
</button>
}
</div>
<ol class="pattern-log">
@for (type of events(); track $index) {
<li>{{ type }}</li>
}
</ol>
`,
})
export class Programmatic {
readonly photos = photos;
readonly label = itemLabel;
readonly locked = signal(false);
readonly events = signal<string[]>([]);
log(event: Event): void {
this.events.update((events) => [...events, event.type].slice(-20));
}
// beforeItemChange comes before every move between items; canceling it keeps the viewer still.
guard(event: FluidboxItemChangeEvent): void {
this.log(event);
if (this.locked()) event.preventDefault();
}
}programmatic.html
<div class="pattern-toolbar">
<button type="button" data-action="open-third">Open the third</button>
<label><input type="checkbox" name="lock" /> Lock navigation</label>
<output name="status">closed -</output>
</div>
<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>
<ol class="pattern-log"></ol>programmatic.ts
export function programmatic(root: HTMLElement) {
const gallery = root.querySelector("fluidbox-gallery")!;
const lock = root.querySelector<HTMLInputElement>('input[name="lock"]')!;
const status = root.querySelector<HTMLOutputElement>('output[name="status"]')!;
const log = root.querySelector<HTMLOListElement>(".pattern-log")!;
root.querySelector('[data-action="open-third"]')!.addEventListener("click", () => {
void gallery.open(2);
});
// beforeitemchange comes before every move between items; canceling it keeps the viewer still.
gallery.addEventListener("beforeitemchange", (event) => {
if (lock.checked) event.preventDefault();
});
for (const type of ["beforetoggle", "toggle", "beforeitemchange", "itemchange", "settle"]) {
gallery.addEventListener(type, () => {
log.append(Object.assign(document.createElement("li"), { textContent: type }));
while (log.children.length > 20) log.firstElementChild!.remove();
});
}
gallery.subscribe(({ status: current, index }) => {
status.value = `${current} ${index ?? "-"}`;
});
}programmatic.ts
import { createFluidbox, itemLabel } from "@ogdakke/fluidbox";
import { photos } from "./data";
export function mountProgrammatic(root: HTMLElement) {
root.insertAdjacentHTML(
"beforeend",
`<div class="pattern-toolbar">
<button type="button" data-action="open-third">Open the third photo</button>
<label><input type="checkbox" name="lock" /> Lock navigation</label>
<output name="status"></output>
</div>
<div class="pattern-grid"></div>
<ol class="pattern-log"></ol>`,
);
const openThird = root.querySelector<HTMLButtonElement>('[data-action="open-third"]')!;
const lock = root.querySelector<HTMLInputElement>('input[name="lock"]')!;
const status = root.querySelector<HTMLOutputElement>('output[name="status"]')!;
const grid = root.querySelector<HTMLElement>(".pattern-grid")!;
const log = root.querySelector<HTMLOListElement>(".pattern-log")!;
const fluidbox = createFluidbox({ items: photos });
// beforeitemchange comes before every move between items; canceling it keeps the viewer still.
fluidbox.addEventListener("beforeitemchange", (event) => {
if (lock.checked) event.preventDefault();
});
for (const type of [
"beforetoggle",
"toggle",
"beforeitemchange",
"itemchange",
"settle",
] as const) {
fluidbox.addEventListener(type, (event) => {
const entry = document.createElement("li");
entry.textContent = event.type;
log.append(entry);
while (log.children.length > 20) log.firstElementChild!.remove();
});
}
const showStatus = () => {
const { status: current, index } = fluidbox.state;
status.value = `${current} ${index ?? "-"}`;
};
showStatus();
fluidbox.subscribe(showStatus);
openThird.addEventListener("click", () => void fluidbox.open(2));
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);
});
return () => {
fluidbox.destroy();
root.replaceChildren();
};
}- Commands.
open(target),next(),previous()andclose()return a promise that resolvestruewhen the command happened andfalsewhen it didn't. A target is an index or an id:open(2),open("photo-fjord"). While open,openmoves to the item. - State.
status(closed,opening,open,closing),id,index,item,count,hasPreviousandhasNext. Read it withuseFluidbox()in the frameworks,gallery.stateandgallery.subscribe()on custom elements, orfluidbox.stateandfluidbox.subscribe()in plain JavaScript. - Events.
beforetoggleandtoggleas the viewer opens and closes, like a popover's;beforeitemchange,itemchangeandsettleas it moves between items. Each has areason:"keyboard","pointer","trigger","api"and so on. The framework roots take them as event props. - Veto.
beforetoggleandbeforeitemchangefire before every command, whether it comes from a key, a gesture or your code. Callevent.preventDefault()to cancel it.