Patterns
Custom content
Besides images, a slide can hold a video, an iframe, or your own content. Custom content is an item with type: "custom" and render: "note". The viewer looks up the note renderer and draws it into the slide.
Loading the live example…
renderers.tsx
import { createContext, useContext } from "react";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/react";
import { media, theme, thumbnailSrc } from "./data";
const ThemeContext = createContext("light");
/** Custom content renders inside the app, so it reads the app's context. */
function Note() {
const theme = useContext(ThemeContext);
return <article className="pattern-note">Theme: {theme}</article>;
}
export function Renderers() {
return (
<ThemeContext.Provider value={theme}>
{/* Items of type "custom" whose render is "note" render <Note />. */}
<Fluidbox.Root items={media} renderers={{ note: () => <Note /> }}>
<div className="pattern-grid">
{media.map((item) => (
<Fluidbox.Trigger
key={item.id}
item={item}
className="pattern-thumb"
>
<img src={thumbnailSrc(item)} alt={itemLabel(item)} />
</Fluidbox.Trigger>
))}
</div>
</Fluidbox.Root>
</ThemeContext.Provider>
);
}Renderers.vue
<script setup lang="ts">
import { defineComponent, h, inject, provide, type InjectionKey } from "vue";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox, type FluidboxVueRenderer } from "@ogdakke/fluidbox/vue";
import { media, theme, thumbnailSrc } from "./data";
// Provided here, injected by any component below, such as the note renderer.
const ThemeKey: InjectionKey<string> = Symbol("theme");
provide(ThemeKey, theme);
const Note = defineComponent(() => {
const current = inject(ThemeKey);
return () => h("article", { class: "pattern-note" }, `Theme: ${current}`);
});
// Draws items of type "custom" whose render is "note".
const renderers: Record<string, FluidboxVueRenderer> = { note: () => h(Note) };
</script>
<template>
<Fluidbox.Root :items="media" :renderers="renderers">
<div class="pattern-grid">
<Fluidbox.Trigger
v-for="item in media"
:key="item.id"
:item="item"
class="pattern-thumb"
>
<img :src="thumbnailSrc(item)" :alt="itemLabel(item)" />
</Fluidbox.Trigger>
</div>
</Fluidbox.Root>
</template>renderers.tsx
import { createContext, For, useContext } from "solid-js";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/solid";
import { media, theme, thumbnailSrc } from "./data";
const ThemeContext = createContext("light");
function Note() {
// Renderers run under Root, so context provided around it reaches them.
const theme = useContext(ThemeContext);
return <article class="pattern-note">Theme: {theme}</article>;
}
export function Renderers() {
return (
<ThemeContext.Provider value={theme}>
{/* Draws items of type "custom" whose render is "note". */}
<Fluidbox.Root items={media} renderers={{ note: () => <Note /> }}>
<div class="pattern-grid">
<For each={media}>
{(item) => (
<Fluidbox.Trigger item={item} class="pattern-thumb">
<img src={thumbnailSrc(item)} alt={itemLabel(item)} />
</Fluidbox.Trigger>
)}
</For>
</div>
</Fluidbox.Root>
</ThemeContext.Provider>
);
}renderers.ts
import { Component, InjectionToken, inject } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import { FluidboxModule } from "@ogdakke/fluidbox/angular";
import { media, theme, thumbnailSrc } from "./data";
export const THEME = new InjectionToken<string>("THEME");
/** Custom content renders inside the root, so it injects from the app. */
@Component({
selector: "article[appNote]",
standalone: true,
host: { class: "pattern-note" },
template: `Theme: {{ theme }}`,
})
export class Note {
readonly theme = inject(THEME);
}
@Component({
selector: "pattern-renderers",
standalone: true,
imports: [FluidboxModule],
providers: [{ provide: THEME, useValue: theme }],
template: `
<!-- Items of type "custom" whose render is "note" render <article appNote>. -->
<div fluidboxRoot [items]="media" [renderers]="renderers" class="pattern-grid">
@for (item of media; track item.id) {
<button [fluidboxTrigger]="item" class="pattern-thumb">
<img [src]="thumbnailSrc(item)" [alt]="label(item)" />
</button>
}
</div>
`,
})
export class Renderers {
readonly media = media;
readonly renderers = { note: Note };
readonly label = itemLabel;
readonly thumbnailSrc = thumbnailSrc;
}renderers.html
<fluidbox-gallery class="pattern-grid">
<button
type="button"
class="pattern-thumb"
data-fluidbox-item-id="media-note"
>
<img src="/art/meadow.svg" alt="A note" />
</button>
<button type="button" class="pattern-thumb" data-fluidbox-item-id="media-map">
<img src="/map/preview.svg" alt="Helsinki Market Square on OpenStreetMap" />
</button>
<button
type="button"
class="pattern-thumb"
data-fluidbox-item-id="media-video"
>
<img src="/video/drift-poster.jpg" alt="Drifting clouds" />
</button>
<button
type="button"
class="pattern-thumb"
data-fluidbox-item-id="media-youtube"
>
<img
src="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg"
alt="Rick Astley - Never Gonna Give You Up"
/>
</button>
</fluidbox-gallery>renderers.ts
import { media, theme } from "./data";
export function renderers(root: HTMLElement) {
const gallery = root.querySelector("fluidbox-gallery")!;
// Items from JS; the buttons marked data-fluidbox-item-id open them.
gallery.items = media;
gallery.renderers = {
note({ target }) {
const note = document.createElement("article");
note.className = "pattern-note";
note.textContent = `Theme: ${theme}`;
target.append(note);
},
};
}renderers.ts
import { createFluidbox, itemLabel, type FluidboxRenderer } from "@ogdakke/fluidbox";
import { media, theme, thumbnailSrc } from "./data";
// Draws items of type "custom" whose render is "note".
const note: FluidboxRenderer = ({ target }) => {
const article = document.createElement("article");
article.className = "pattern-note";
article.textContent = `Theme: ${theme}`;
target.append(article);
return () => article.remove();
};
export function mountRenderers(root: HTMLElement) {
const fluidbox = createFluidbox({ items: media, renderers: { note } });
const grid = document.createElement("div");
grid.className = "pattern-grid";
media.forEach((item, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = "pattern-thumb";
button.append(Object.assign(new Image(), { src: thumbnailSrc(item), alt: itemLabel(item) }));
grid.append(button);
fluidbox.bind(button, index);
});
root.append(grid);
return () => {
fluidbox.destroy();
grid.remove();
};
}- Framework components keep their context. React and Vue render custom slides through portals inside
Root, and Solid renders them underRoot's owner, so context, provide/inject and stores work. Angular renders anng-template, which keeps the injector of the place you declare it. - Plain renderers are functions that get
{ item, index, target, signal }, append totarget, and may return a function that releases their own resources. The viewer emptiestargetitself. - A YouTube embed is an iframe item. YouTube won't play an embed that sends no referrer, so the example data sets
referrerPolicy: "strict-origin-when-cross-origin"on it. - Markup. On custom elements, any element other than an image, video or iframe in a
<fluidbox-item>'s content template is custom content. Withgallery.items, setgallery.renderers. - Every type needs
widthandheightso the viewer can size the slide before anything loads. Videos show theirposteruntil they play.
See Media types for every field.