Patterns
Rich origins
The origin is the element the viewer flies from and back to. By default it is the trigger's first image or video, or the trigger itself. A card can mark a different element, and a scroller gets its closing item scrolled back into view.
Loading the live example…
origins.tsx
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/react";
import { product, strip } from "./data";
const products = [product];
/** Only the photo flies: the card marks it with data-fluidbox-origin. */
export function ProductCard() {
return (
<Fluidbox.Root items={products}>
<Fluidbox.Trigger item={product} asChild>
<button type="button" className="pattern-card">
<span data-fluidbox-origin>
<img src={product.src} alt={itemLabel(product)} />
</span>
<span>
<strong>Coral runner</strong>
<br />
€120
</span>
</button>
</Fluidbox.Trigger>
</Fluidbox.Root>
);
}
/** Moving through the viewer scrolls each trigger into view, so the close lands on it. */
export function Scroller() {
return (
<Fluidbox.Root items={strip}>
<div className="pattern-scroller">
{strip.map((photo) => (
<Fluidbox.Trigger
key={photo.src}
item={photo}
className="pattern-thumb"
>
<img src={photo.src} alt={itemLabel(photo)} />
</Fluidbox.Trigger>
))}
</div>
</Fluidbox.Root>
);
}Origins.vue
<script setup lang="ts">
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/vue";
import { product, strip } from "./data";
</script>
<template>
<!-- The photo flies from the element marked data-fluidbox-origin, not the whole card. -->
<Fluidbox.Root :items="[product]">
<Fluidbox.Trigger :item="product" class="pattern-card">
<span data-fluidbox-origin>
<img :src="product.src" :alt="itemLabel(product)" />
</span>
<span><strong>Coral runner</strong><br />€120</span>
</Fluidbox.Trigger>
</Fluidbox.Root>
<!-- Moving through the viewer scrolls each trigger into view, so the close lands on it. -->
<Fluidbox.Root :items="strip">
<div class="pattern-scroller">
<Fluidbox.Trigger
v-for="photo in strip"
:key="photo.id"
:item="photo"
class="pattern-thumb"
>
<img :src="photo.src" :alt="itemLabel(photo)" />
</Fluidbox.Trigger>
</div>
</Fluidbox.Root>
</template>origins.tsx
import { For } from "solid-js";
import { itemLabel } from "@ogdakke/fluidbox";
import { Fluidbox } from "@ogdakke/fluidbox/solid";
import { product, strip } from "./data";
export function Origins() {
return (
<>
{/* The photo flies from the element marked data-fluidbox-origin, not the whole card. */}
<Fluidbox.Root items={[product]}>
<Fluidbox.Trigger item={product} class="pattern-card">
<span data-fluidbox-origin>
<img src={product.src} alt={itemLabel(product)} />
</span>
<span>
<strong>Coral runner</strong>
<br />
€120
</span>
</Fluidbox.Trigger>
</Fluidbox.Root>
{/* Moving through the viewer scrolls each trigger into view, so the close lands on it. */}
<Fluidbox.Root items={strip}>
<div class="pattern-scroller">
<For each={strip}>
{(photo) => (
<Fluidbox.Trigger item={photo} class="pattern-thumb">
<img src={photo.src} alt={itemLabel(photo)} />
</Fluidbox.Trigger>
)}
</For>
</div>
</Fluidbox.Root>
</>
);
}origins.ts
import { Component } from "@angular/core";
import { itemLabel } from "@ogdakke/fluidbox";
import { FluidboxModule } from "@ogdakke/fluidbox/angular";
import { product, strip } from "./data";
@Component({
selector: "pattern-origins",
standalone: true,
imports: [FluidboxModule],
template: `
<!-- The photo flies from the element marked data-fluidbox-origin, not the whole card. -->
<div fluidboxRoot [items]="[product]">
<button [fluidboxTrigger]="product" class="pattern-card">
<span data-fluidbox-origin>
<img [src]="product.src" [alt]="label(product)" />
</span>
<span><strong>Coral runner</strong><br />€120</span>
</button>
</div>
<!-- Moving through the viewer scrolls each trigger into view, so the close lands on it. -->
<div fluidboxRoot [items]="strip" class="pattern-scroller">
@for (item of strip; track item.id) {
<button [fluidboxTrigger]="item" class="pattern-thumb">
<img [src]="item.src" [alt]="label(item)" />
</button>
}
</div>
`,
})
export class Origins {
readonly product = product;
readonly strip = strip;
readonly label = itemLabel;
}origins.html
<fluidbox-gallery>
<fluidbox-item item-id="product-runner">
<button slot="trigger" type="button" class="pattern-card">
<span data-fluidbox-origin>
<img src="/runner.svg" alt="Illustrated coral running shoe" />
</span>
<span>
<strong>Coral runner</strong><br />
€120
</span>
</button>
<template slot="content">
<img src="/runner.svg" alt="Illustrated coral running shoe" width="1000" height="700" />
</template>
</fluidbox-item>
</fluidbox-gallery>
<fluidbox-gallery class="pattern-scroller">
<fluidbox-item item-id="strip-alpine">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/alpine.svg" alt="Alpine ridge at dawn" />
</button>
<template slot="content">
<img src="/art/alpine.svg" alt="Alpine ridge at dawn" width="1920" height="1080" />
</template>
</fluidbox-item>
<fluidbox-item item-id="strip-meadow">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/meadow.svg" alt="Meadow hills" />
</button>
<template slot="content">
<img src="/art/meadow.svg" alt="Meadow hills" width="1600" height="1200" />
</template>
</fluidbox-item>
<fluidbox-item item-id="strip-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="strip-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="strip-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="strip-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-item item-id="strip-dune">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/dune.svg" alt="Dune at golden hour" />
</button>
<template slot="content">
<img src="/art/dune.svg" alt="Dune at golden hour" width="1800" height="1200" />
</template>
</fluidbox-item>
<fluidbox-item item-id="strip-canyon">
<button slot="trigger" type="button" class="pattern-thumb">
<img src="/art/canyon.svg" alt="Canyon walls" />
</button>
<template slot="content">
<img src="/art/canyon.svg" alt="Canyon walls" width="1400" height="1400" />
</template>
</fluidbox-item>
</fluidbox-gallery>origins.ts
import { createFluidbox, itemLabel } from "@ogdakke/fluidbox";
import { product, strip } from "./data";
export function mountOrigins(root: HTMLElement) {
// The photo flies from the element marked data-fluidbox-origin, not the whole card.
const productViewer = createFluidbox({ items: [product] });
const card = document.createElement("button");
card.type = "button";
card.className = "pattern-card";
card.innerHTML = `
<span data-fluidbox-origin><img src="${product.src}" alt="${itemLabel(product)}" /></span>
<span><strong>Coral runner</strong><br />€120</span>`;
productViewer.bind(card, 0);
// Moving through the viewer scrolls each trigger into view, so the close lands on it.
const stripViewer = createFluidbox({ items: strip });
const scroller = document.createElement("div");
scroller.className = "pattern-scroller";
strip.forEach((item, index) => {
const button = document.createElement("button");
button.type = "button";
button.className = "pattern-thumb";
button.append(Object.assign(new Image(), { src: item.src, alt: itemLabel(item) }));
scroller.append(button);
stripViewer.bind(button, index);
});
root.append(card, scroller);
return () => {
productViewer.destroy();
stripViewer.destroy();
card.remove();
scroller.remove();
};
}data-fluidbox-originon any element inside the trigger makes it the origin. The flight copies its box, crop (object-fit,object-position) and corner radius.- Reveal on close. Each time the selection settles, and on close if it hasn't yet, the viewer scrolls the item's trigger into view (nearest edge), so the closing flight lands on it. To control this yourself, give the viewer an
originsadapter withrevealOrigin(id, index, { signal, reason })(see The origin adapter). - As child. The framework
Triggerparts render a button by default, and withasChilddecorate your own element instead. See each framework page. Angular'sfluidboxTriggeris an attribute, so it always decorates. In plain JavaScript,fluidbox.bind()always decorates the element you give it.