Guides
Media types
An item is one of four types: "img", "video", "iframe" or "custom". They share one slide strip, one set of gestures and the same flights. Their fields are the attributes of the element the viewer draws, by DOM property name, so srcset, poster and referrerPolicy mean what they mean on <img>, <video> and <iframe>.
Every type needs width and height in pixels. The viewer uses their ratio to size the slide before anything loads.
Every type can also have:
| Field | Type | What it does |
|---|---|---|
id |
string |
The item's identity across updates, for open("id"), events and triggers. Defaults to src. Unique in a list. |
thumbnail |
FluidboxThumbnail |
A small image of the item. See Thumbnails and placeholders. |
placeholder |
string |
An image URL, such as a data URI, painted at once behind an image slide. |
Image#
FluidboxImageItem, drawn as an <img>, or a <picture> when it has sources:
| Field | Type | What it does |
|---|---|---|
type |
"img" |
Required. |
src |
string |
Required. The full image. |
alt |
string |
Required. Alt text. Also the filmstrip button's label. |
width |
number |
Required. Intrinsic width. |
height |
number |
Required. Intrinsic height. |
srcset |
string |
The <img>'s srcset. |
sizes |
string |
The <img>'s sizes. "auto" works too. |
sources |
FluidboxPictureSource[] |
<source> elements of a <picture>, such as AVIF and WebP. |
Each FluidboxPictureSource has srcset (required), and sizes, media and type, as on <source>.
import type { FluidboxImageItem } from "@ogdakke/fluidbox";
const photo: FluidboxImageItem = {
type: "img",
id: "footpath",
src: "/photos/footpath-2400.jpg",
srcset: "/photos/footpath-1200.jpg 1200w, /photos/footpath-2400.jpg 2400w",
sizes: "100vw",
alt: "A footpath through tall grass",
width: 2400,
height: 1600,
sources: [
{
type: "image/avif",
srcset: "/photos/footpath-1200.avif 1200w, /photos/footpath-2400.avif 2400w",
},
],
thumbnail: { src: "/photos/footpath-400.jpg", width: 400, height: 267 },
};Only images zoom. The viewer loads the full image for the current slide and the one on each side, and keeps the picture it already shows until the sharper one has decoded. If the full image fails, the slide shows "Unable to load media." with a Retry button.
Video#
FluidboxVideoItem, drawn as a <video>:
| Field | Type | Default | What it does |
|---|---|---|---|
type |
"video" |
Required. | |
src |
string |
Required. The video file. | |
title |
string |
Required. The video's accessible name, and the filmstrip button's. | |
width |
number |
Required. Intrinsic width. | |
height |
number |
Required. Intrinsic height. | |
poster |
string |
The poster frame's URL. | |
autoplay |
boolean |
false |
Plays when its slide comes into view, and pauses when it leaves. |
loop |
boolean |
false |
Starts again at the end. |
muted |
boolean |
false |
Starts muted. Browsers let a video autoplay only when it is muted. |
playsInline |
boolean |
true |
Plays in the slide rather than full screen on iOS. |
controls |
boolean |
true |
Shows the browser's playback controls. |
preload |
"none" | "metadata" | "auto" |
"metadata" |
How much to load before it plays. |
import type { FluidboxVideoItem } from "@ogdakke/fluidbox";
const clip: FluidboxVideoItem = {
type: "video",
src: "/video/harbour.mp4",
title: "Harbour timelapse",
width: 1920,
height: 1080,
poster: "/video/harbour.jpg",
thumbnail: { src: "/video/harbour-thumb.jpg", width: 480, height: 270 },
};A video pauses when its slide leaves, and unloads when it leaves the loaded window. Moving a playing video would restart it, so a video closes inside its own frame instead of in the flight.
Iframe#
FluidboxIframeItem, drawn as an <iframe>:
| Field | Type | What it does |
|---|---|---|
type |
"iframe" |
Required. |
src |
string |
Required. The embedded page. |
title |
string |
Required. The iframe's title. |
width |
number |
Required. The frame's width. |
height |
number |
Required. The frame's height. |
allow |
string |
The iframe's allow attribute. |
referrerPolicy |
ReferrerPolicy |
The iframe's referrerpolicy. |
sandbox |
string |
The iframe's sandbox attribute. |
import type { FluidboxIframeItem } from "@ogdakke/fluidbox";
const map: FluidboxIframeItem = {
type: "iframe",
id: "market-map",
src: "https://www.openstreetmap.org/export/embed.html?bbox=24.94,60.16,24.96,60.17",
title: "Market Square on OpenStreetMap",
width: 1000,
height: 700,
thumbnail: { src: "/map/market.png", width: 1000, height: 700 },
};The iframe gets loading="lazy" and allowfullscreen. An embedded page paints well after the opening flight starts, so the viewer covers the frame with a still image until the page has loaded, then fades it out. On close it fades the still back in, so the frame lands as the picture it left from. The still is the item's thumbnail, else the trigger's image. Use a screenshot of what the embed first shows.
Custom#
FluidboxCustomItem names a renderer that draws your own content into the slide:
| Field | Type | What it does |
|---|---|---|
type |
"custom" |
Required. |
id |
string |
Required. Custom content has no src to fall back on. |
render |
string |
Required. The key of a renderer in renderers. |
title |
string |
Required. The content's accessible name. |
width |
number |
Required. The width the content is laid out at. |
height |
number |
Required. The height the content is laid out at. |
Renderers#
A renderer is a function. It gets { item, index, target, signal } and draws into target, the slide's element. You may return a function that releases your own resources, such as timers or listeners. The viewer empties target itself afterwards. signal aborts at the same moment.
import { createFluidbox, type FluidboxRenderer } from "@ogdakke/fluidbox";
const note: FluidboxRenderer = ({ item, target }) => {
const article = document.createElement("article");
const tick = () => (article.textContent = `${item.id}, ${new Date().toLocaleTimeString()}`);
tick();
const timer = setInterval(tick, 1000);
target.append(article);
return () => clearInterval(timer);
};
const fluidbox = createFluidbox({
items: [{ type: "custom", id: "note", render: "note", title: "A note", width: 800, height: 600 }],
renderers: { note },
});The same renderer shape fills filmstrip thumbnails, the sheet and travelling overlays. The framework roots take renderers as framework components, keyed the same way, and they keep your app's context. When a binding needs a wrapper, as React does, the wrapper has data-fluidbox-mount and display: contents, so your component's element still sizes to the slide as plain content does. An item whose renderer is missing throws a TypeError when its slide loads.
A custom slide mounts when it comes within one slide of the current one, and its cleanup runs when it moves further away. Keep state you need in your app, not in the mounted nodes. Clicks on buttons, links and fields inside custom content never count as zoom taps.
Custom content shows a framework component, a map, a video and a YouTube embed in every framework.
Thumbnails and placeholders#
thumbnail is a FluidboxThumbnail: src, plus optional srcset, sizes, width and height.
| Field | Used for |
|---|---|
thumbnail |
The filmstrip, the iframe still, and the picture shown while the full image loads. |
placeholder |
Image items only. Painted at once behind the slide, such as a blurred 32-pixel image or a data URI. |
While an image opens, the slide paints, from the bottom up: the placeholder, then the trigger's image or the thumbnail, then the full image once it has decoded. The frame's background, --fluidbox-image-placeholder-background, shows when there is none of them.
The filmstrip tries the thumbnail, then the full image or the video's poster, the smallest srcset candidate of each first. The slide and the iframe still show the thumbnail with its srcset and sizes, so the browser picks the candidate that fits.
Items from <img> props#
imageItem(image, thumbnail?) from @ogdakke/fluidbox builds an image item from the props an <img> takes, in React's spelling (srcSet) or the HTML one (srcset). image is the full-size picture for the viewer. thumbnail, at the triggers' sizes, becomes the item's thumbnail. A url() in either one's style.backgroundImage becomes the placeholder. That is where next/image puts its blur.
import { imageItem } from "@ogdakke/fluidbox";
const item = imageItem(
{
src: "/harbour-1600.jpg",
srcSet: "/harbour-800.jpg 800w, /harbour-1600.jpg 1600w",
sizes: "100vw",
width: 1600,
height: 1200,
alt: "Harbour",
},
{ src: "/harbour-400.jpg", sizes: "25vw" },
);It throws a TypeError without a width and height. It returns a plain item, so spread it into an object of your own to add fields. Next.js shows it with getImageProps.
Fields of your own#
Items are your own objects. Add any field, and the viewer hands your object back in every state, event and callback. Type them by extending an item type, and the type flows through:
import { createFluidbox, type FluidboxImageItem } from "@ogdakke/fluidbox";
interface Photo extends FluidboxImageItem {
readonly caption: string;
}
declare const photos: Photo[];
createFluidbox<Photo>({
items: photos,
viewer({ target, fluidbox }) {
const caption = document.createElement("p");
target.append(caption);
return fluidbox.subscribe(({ item }) => {
if (item) caption.textContent = item.caption;
});
},
});Content in the viewer fades in and out with the controls. See Styling. Headless controls builds a caption bar in every framework.