Patterns
Sheet
The sheet is an optional add-on from @ogdakke/fluidbox/sheet. It is a panel the viewer slides up from the bottom, with snap points, for details, comments or a form.
Loading the live example…
sheet.tsx
import { itemLabel, type FluidboxItem } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidboxSheet } from "@ogdakke/fluidbox/react";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";
function Details({ item }: { item: FluidboxItem }) {
const sheet = useFluidboxSheet();
return (
<>
<h3>{itemLabel(item)}</h3>
<p>
Stop <output>{sheet.stop}</output>
</p>
<button type="button" onClick={() => sheet.set(2)}>
Expand
</button>
<input placeholder="Add a note" aria-label="Note" />
</>
);
}
export function Sheet() {
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>
{/* Stop 0 rests below the photo; stops 1 and 2 raise it to 40% and 90% of the screen. */}
<Fluidbox.Sheet snapPoints={["40%", "90%"]} className="pattern-sheet">
{({ item }) => <Details item={item} />}
</Fluidbox.Sheet>
<Fluidbox.Viewer>
<Fluidbox.SheetStop stop={1} className="pattern-sheet-toggle">
Details
</Fluidbox.SheetStop>
<Fluidbox.Controls />
</Fluidbox.Viewer>
</Fluidbox.Root>
);
}Sheet.vue
<script setup lang="ts">
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/vue";
import "@ogdakke/fluidbox/sheet.css";
import { ref } from "vue";
import { photos } from "./data";
// The stop the sheet rests at: 0 at rest, then one per snap point.
const stop = ref(0);
</script>
<template>
<Fluidbox.Root :items="photos" @sheetchange="stop = $event.stop">
<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.Viewer>
<Fluidbox.Controls />
<!-- At stop 0 the sheet is out of sight; this raises it. -->
<Fluidbox.SheetStop :stop="1" class="pattern-sheet-toggle">
Details
</Fluidbox.SheetStop>
</Fluidbox.Viewer>
<!-- The sheet's content, for the active item. Its attributes go on the sheet. -->
<Fluidbox.Sheet v-slot="{ item }" :snap-points="['40%', '90%']" class="pattern-sheet">
<h3>{{ itemLabel(item) }}</h3>
<p>
Stop <output>{{ stop }}</output>
</p>
<Fluidbox.SheetStop :stop="2">Expand</Fluidbox.SheetStop>
<input placeholder="Add a note" aria-label="Note" />
</Fluidbox.Sheet>
</Fluidbox.Root>
</template>sheet.tsx
import { For } from "solid-js";
import { itemLabel, type FluidboxImageItem } from "@ogdakke/fluidbox";
import { Fluidbox, useFluidboxSheet } from "@ogdakke/fluidbox/solid";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";
function Details(props: { photo: FluidboxImageItem }) {
const sheet = useFluidboxSheet();
return (
<>
<h3>{itemLabel(props.photo)}</h3>
<p>
Stop <output>{sheet.stop()}</output>
</p>
<button type="button" onClick={() => sheet.set(2)}>
Expand
</button>
<input placeholder="Add a note" aria-label="Note" />
</>
);
}
export function Sheet() {
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>
{/* The sheet's content, for the active item. Its attributes go on the sheet itself. */}
<Fluidbox.Sheet<FluidboxImageItem> snapPoints={["40%", "90%"]} class="pattern-sheet">
{({ item }) => <Details photo={item} />}
</Fluidbox.Sheet>
<Fluidbox.Viewer>
{/* Raises the sheet to its first stop. */}
<Fluidbox.SheetStop stop={1} class="pattern-sheet-toggle">
Details
</Fluidbox.SheetStop>
<Fluidbox.Controls />
</Fluidbox.Viewer>
</Fluidbox.Root>
);
}sheet.ts
import { Component, Input } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import {
FluidboxModule,
injectFluidboxSheet,
type FluidboxImageItem,
} from "@ogdakke/fluidbox/angular";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";
@Component({
selector: "div[appDetails]",
standalone: true,
host: { class: "pattern-sheet" },
template: `
<h3>{{ label(photo) }}</h3>
<p>
Stop <output>{{ sheet.stop() }}</output>
</p>
<button type="button" (click)="sheet.set(2)">Expand</button>
<input placeholder="Add a note" aria-label="Note" />
`,
})
export class Details {
@Input({ required: true }) photo!: FluidboxImageItem;
readonly sheet = injectFluidboxSheet();
readonly label = itemLabel;
}
@Component({
selector: "pattern-sheet",
standalone: true,
imports: [FluidboxModule, Details],
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>
}
<!-- Stop 0 rests below the photo; stops 1 and 2 raise it to 40% and 90% of the screen. -->
<ng-template fluidboxSheet [snapPoints]="['40%', '90%']" let-photo>
<div appDetails [photo]="photo"></div>
</ng-template>
<ng-template fluidboxViewer>
<div fluidboxControls></div>
<!-- A fluidboxSheetStop button moves the sheet to that stop. -->
<button fluidboxSheetStop="1" class="pattern-sheet-toggle">Details</button>
</ng-template>
</div>
`,
})
export class Sheet {
readonly photos = photos;
readonly label = itemLabel;
}sheet.html
<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>
<template slot="viewer">
<fluidbox-controls></fluidbox-controls>
<button
type="button"
class="pattern-sheet-toggle"
data-fluidbox-sheet-stop="1"
>
Details
</button>
</template>
<fluidbox-sheet snap-points="40% 90%">
<template>
<div class="pattern-sheet">
<h3></h3>
<p>Stop <output name="stop">0</output></p>
<button type="button" data-fluidbox-sheet-stop="2">Expand</button>
<input type="text" aria-label="Note" placeholder="Add a note" />
</div>
</template>
</fluidbox-sheet>
</fluidbox-gallery>sheet.ts
import { itemLabel } from "@ogdakke/fluidbox";
import "@ogdakke/fluidbox/sheet.css";
export function sheet(root: HTMLElement) {
const gallery = root.querySelector("fluidbox-gallery")!;
const sheet = gallery.querySelector("fluidbox-sheet")!;
const content = sheet.querySelector("template")!;
// Fill the sheet for the active item; without this it shows the template as is.
sheet.render = ({ item, target, signal }) => {
target.append(content.content.cloneNode(true));
target.querySelector("h3")!.textContent = itemLabel(item);
const stop = target.querySelector<HTMLOutputElement>('output[name="stop"]')!;
stop.value = String(gallery.sheet!.stop);
gallery.addEventListener("sheetchange", (event) => (stop.value = String(event.stop)), {
signal,
});
};
}sheet.ts
import {
createFluidbox,
itemLabel,
type Fluidbox,
type FluidboxImageItem,
} from "@ogdakke/fluidbox";
import "@ogdakke/fluidbox/sheet";
import "@ogdakke/fluidbox/sheet.css";
import { photos } from "./data";
export function mountSheet(root: HTMLElement) {
const fluidbox: Fluidbox<FluidboxImageItem> = createFluidbox({
items: photos,
sheet: {
snapPoints: ["40%", "90%"],
// Fills the sheet for the active item.
render({ item, target, signal }) {
const sheet = fluidbox.sheet!;
target.innerHTML = `
<div class="pattern-sheet">
<h3></h3>
<p>Stop <output name="stop"></output></p>
<button type="button" data-action="expand">Expand</button>
<input placeholder="Add a note" aria-label="Note" />
</div>`;
target.querySelector("h3")!.textContent = itemLabel(item);
target
.querySelector('[data-action="expand"]')!
.addEventListener("click", () => sheet.set(2));
const stop = target.querySelector<HTMLOutputElement>('output[name="stop"]')!;
stop.value = String(sheet.stop);
fluidbox.addEventListener("sheetchange", (event) => (stop.value = String(event.stop)), {
signal,
});
},
},
// A button marked data-fluidbox-sheet-stop moves the sheet to that stop.
viewer({ target }) {
target.insertAdjacentHTML(
"beforeend",
`<button type="button" class="pattern-sheet-toggle" data-fluidbox-sheet-stop="1">Details</button>`,
);
},
});
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 () => {
fluidbox.destroy();
grid.remove();
};
}snapPoints: ["40%", "90%"]adds stops above the resting stop0. Set it in thesheetoption in plain JavaScript, as theSheetpart's prop, or assnap-points="40% 90%"on<fluidbox-sheet>.fluidbox.sheet.set(stop)moves the sheet, and asheetchangeevent reports each new stop. In the frameworks,useFluidboxSheet()returns{ stop, set }and the root takesonSheetChange,@sheetchangeor(sheetChange). On custom elements,gallery.sheetis the same handle and the gallery firessheetchange.- Any button with
data-fluidbox-sheet-stop="N"in the viewer moves the sheet to stopN, and lowers it again when the sheet is already there. The frameworkSheetStoppart renders one. - Keys typed in the sheet's fields stay there, so arrow keys don't change the slide.
import "@ogdakke/fluidbox/sheet"once to turn the add-on on (the custom elements load it themselves), and import@ogdakke/fluidbox/sheet.csswith it.