Flat Card
No decoration
Default variant with no border or shadow.
A fluid type scale generated with Utopia. Sizes interpolate smoothly between 320px and 1240px viewports.
step-5
The quick brown fox
Display / H1 step-4
The quick brown fox
H2 step-3
The quick brown fox
H3 step-2
The quick brown fox
H4 step-1
The quick brown fox
H5 / Large text step-0
The quick brown fox
Body / H6 step--1
The quick brown fox
~15px intermediate step--2
The quick brown fox
Small text step--3
The quick brown fox
Caption / Fine print --ff-regular Open Sans The quick brown fox jumps over the lazy dog. 0123456789
--ff-semibold Open Sans SemiBold The quick brown fox jumps over the lazy dog. 0123456789
--ff-serif Georgia The quick brown fox jumps over the lazy dog. 0123456789
The quick brown fox jumps over the lazy dog. (italic)
--ff-system System UI The quick brown fox jumps over the lazy dog. 0123456789
Common text style combinations used throughout the site.
"Photography is the story I fail to put into words."
Transform your favorite photos into stunning wall art
Our prints are made with museum-quality materials that last a lifetime.
--fw-thin (300) The quick brown fox jumps over the lazy dog
--fw-regular (400) The quick brown fox jumps over the lazy dog
--fw-semibold (500) The quick brown fox jumps over the lazy dog
--fw-bold (550) The quick brown fox jumps over the lazy dog
--fw-bolder (600) The quick brown fox jumps over the lazy dog
--fw-black (700) The quick brown fox jumps over the lazy dog
--lh-tight 1.2 — headings The quick brown fox jumps over the lazy dog. This text demonstrates the tight line height used for headings and display text.
--lh-normal 1.4 — body (inherited from body) The quick brown fox jumps over the lazy dog. This text demonstrates the normal line height set on the body element and inherited everywhere.
--lh-relaxed 1.6 — long-form reading The quick brown fox jumps over the lazy dog. This text demonstrates the relaxed line height used for long-form reading passages.
Headings use the serif font (Georgia) by default.
<h1> <h2> <h3> <h4> <h5> <h6>
Color tokens organized by function. Light mode values defined in :root.
Punchline palette provides warm editorial tones for landing/hero sections.
white bg surface surface-80 surface-60 surface-warm text text-40 text-60 text-80 text-mid text-muted primary primary-30 primary-60 primary-80 primary-90 primary-light primary-dark cta cta-45 cta-65 cta-80 cta-90 cta-hover cta-shadow gold gold-light error error-dark error-mid error-light success success-mid success-dark success-light success-shadow warning warning-hover info info-mid grey-black grey-black-40 grey-black-60 grey-black-80 grey-mid-blue grey-mid-light logo-mark logo-text promo-blue promo-cyan promo-yellow terracotta terracotta-dark gold gold-dark gold-accent warm-light warm-mid warm-muted text-dark text-light cream cream-warm brown warm-text bg-start bg-end badge-text badge-year terracotta-shadow terracotta-shadow-strong gold-border gold-shadow gold-shadow-strong overlay border border-light Design tokens for spacing, radii, shadows, and transitions. Spacing uses a fluid scale generated with Utopia.
Each step has a specific purpose. Smaller values for tight UI elements, larger values for page-level breathing room.
3xs Tight gaps, icon margins 2xs Button padding, small gaps xs Card padding, list gaps s Default gaps, input padding m Section gaps, card spacing l Large section padding xl Hero padding, major sections 2xl Page sections 3xl Full-page vertical rhythm Pairs scale from the first value (320px viewport) to the second (1240px viewport). Use these for responsive spacing that grows with screen size.
3xs → 2xs 2xs → xs xs → s s → m Default flow space m → l l → xl xl → 2xl 2xl → 3xl s → l Custom pair Use space tokens for gap in flex and grid layouts.
Use space tokens for consistent padding inside components.
2xs xs s m
The .flow composition adds vertical spacing between siblings.
Override with .flow-space-* utilities.
<div class="flow">
<p>First paragraph</p>
<p>Default spacing above</p>
<p class="flow-space-xl">Extra spacing above</p>
</div> Four radius tokens for consistent rounding across the design system.
s m l xl | Token | Value | Use |
|---|---|---|
--border-radius-s | 0.25rem | Tags, badges, inline code |
--border-radius-m | 0.5rem | Cards, inputs, buttons |
--border-radius-l | 1rem | Panels, modals |
--border-radius-xl | 1.25rem | Large feature cards |
Two shadow families: neutral for standard UI, warm for editorial/punchline sections.
--shadow-s --shadow-m --shadow-l --shadow-warm-s --shadow-warm-m --shadow-warm-l Speed tokens for consistent animation timing. Never hardcode raw durations.
--transition-speed 250ms — default for hover, focus, color changes --transition-speed-slow 400ms — panel slides, complex transforms --transition-speed-zoom 6s — slow Ken Burns zoom on images transition: background-color var(--transition-speed) ease;
transition: transform var(--transition-speed-slow) ease; Layout primitives that handle structure without visual styling. They answer: "How should these elements be arranged?"
Purpose: Adds consistent vertical spacing between sibling elements.
Use when: You have a stack of content (paragraphs, cards, form fields) that needs breathing room.
<article class="flow">
<h2>Title</h2>
<p>First paragraph...</p>
<p>Second paragraph...</p>
</article> Purpose: Centers content with a max-width and side padding.
Use when: You need to constrain content width for readability.
Variants: data-width="narrow" (60rem) or data-width="wide" (90rem)
<div class="wrapper">
<!-- Content constrained to 75rem -->
</div>
<div class="wrapper" data-width="narrow">
<!-- Content constrained to 60rem -->
</div> Purpose: Groups items horizontally with wrapping.
Use when: Tags, buttons, or badges that should wrap to new lines.
<div class="cluster">
<span class="tag">Photography</span>
<span class="tag">Art</span>
<span class="tag">Print</span>
</div> Purpose: Two-column layout where one column has a fixed width.
Use when: Navigation + content, filters + results, image + text.
Behavior: Stacks vertically when content can't fit side-by-side.
<div class="sidebar" style="--sidebar-width: 15rem;">
<aside>Filters</aside>
<main>Product grid</main>
</div> Purpose: Equal-width columns that stack when viewport is too narrow.
Use when: Feature cards, pricing tiers, comparison columns.
Behavior: Columns until width < threshold, then stacks.
↔ Resize browser to see it stack
<div class="switcher">
<div class="pricing-card">Basic</div>
<div class="pricing-card">Pro</div>
<div class="pricing-card">Enterprise</div>
</div> Purpose: Responsive grid that auto-fills columns based on available space.
Use when: Card grids, image galleries, product listings.
Behavior: Adds/removes columns automatically as viewport changes.
<div class="auto-grid" style="--auto-grid-min-item-size: 16rem;">
<div class="card">...</div>
<div class="card">...</div>
<div class="card">...</div>
</div> Purpose: Explicit column grid with fixed column counts.
Use when: You need exact control over column numbers.
Options: data-columns="1" through "5", plus "40-60" and "60-40".
<div class="column-layout" data-columns="3">...</div>
<div class="column-layout" data-columns="60-40">...</div> Purpose: Breaks out of a .wrapper to span the full viewport width.
Use when: A hero image or background section needs to go edge-to-edge while the rest of the page is wrapped.
<div class="wrapper">
<p>Wrapped content...</p>
<div class="full-bleed">
<!-- Full viewport width -->
</div>
<p>Back to wrapped...</p>
</div> Purpose: Basic flex row with gap. No opinions on wrap or alignment.
Use when: You need a flex container but .cluster or .sidebar are too opinionated.
Custom property: --flex-gap (default: var(--space-s-m))
<div class="flex">
<div>Item A</div>
<div>Item B</div>
</div> Purpose: Basic grid container. No opinions on columns or gap — blocks define these as needed.
Use when: You need a grid container but .auto-grid or .column-layout are too opinionated.
Note: Add gap and grid-template-columns inline or in your block styles.
<div class="grid" style="grid-template-columns: 1fr 2fr; gap: var(--space-m);">
<aside>...</aside>
<main>...</main>
</div> Escape hatches for inheritance overrides. Use sparingly — prefer compositions and blocks.
Text at step-2
Text at step-0 (body)
Text at step--1 (small)
.text-center
.text-step--3 through .text-step-5
.text-center | .text-start | .text-end Constrain inline size for comfortable reading.
.measure-narrow — 45ch. Captions, sidebars, and short labels.
.measure — 65ch. Body text, paragraphs, the default readable width for long-form content.
.measure-wide — 80ch. Subtitles, lead text, wider containers that still benefit from a cap.
.measure-extra-wide — unset. Full width, no cap.
.measure-narrow (max-inline-size: 45ch)
.measure (max-inline-size: 65ch)
.measure-wide (max-inline-size: 80ch)
.measure-extra-wide (max-inline-size: unset)
.center-inline (margin-inline: auto)
.justify-center (justify-items: center)
Override vertical rhythm within a .flow container.
.flow-space-3xs | .flow-space-2xs | .flow-space-xs
.flow-space-s | .flow-space-m | .flow-space-l
.flow-space-xl | .flow-space-2xl | .flow-space-3xl Override gap within flex or grid containers.
.gap-3xs | .gap-2xs | .gap-xs
.gap-s | .gap-m | .gap-l
.gap-xl | .gap-2xl | .gap-3xl Quick layout helpers for common flex and grid patterns.
| Class | Effect |
|---|---|
.flex | display: flex |
.flex-center | Flex with centered items both axes |
.flex-col | Flex column direction |
.flex-between | Flex with space-between, vertically centered |
.grid-center | Grid with place-items: center |
Composable primitives for flex layouts. Combine with .flex from compositions.
| Category | Classes |
|---|---|
| Display | .inline-flex |
| Wrap | .flex-wrap, .flex-nowrap |
| Shrink | .shrink-0 |
| align-items | .items-start, .items-center, .items-end, .items-stretch, .items-baseline |
| justify-content | .content-start, .content-center, .content-end, .content-between |
| align-self | .self-start, .self-center, .self-end |
<div class="flex items-center content-between gap-s">
<span>Left</span>
<span>Right</span>
</div> Font style, line height, and text color overrides.
This text is italic — .italic
.lh-tight (1.2) — The quick brown fox jumps over the lazy dog. Tight line height for headings.
.lh-relaxed (1.6) — The quick brown fox jumps over the lazy dog. Relaxed line height for long-form reading.
.text-white
.text-muted
.text-muted-40
.text-terracotta
| Category | Classes |
|---|---|
| Style | .italic |
| Line height | .lh-1, .lh-tight, .lh-normal, .lh-relaxed |
| Color | .text-white, .text-muted, .text-muted-40, .text-terracotta |
Semantic text patterns for common UI elements.
.u-subtitle Transform your favorite photos into stunning wall art
.u-title .u-price-vat €299.00 incl. VAT
.u-subtitle (serif, step-1, italic)
.u-title (text-wrap: balance)
.u-price-vat (step--2, text-40 color) Small single-purpose utilities.
.font-semibold — switches to the semibold font family
.u-caps — uppercase with configurable tracking (default 0.05em, override via --_caps-tracking)
.u-nowrap — prevents text from wrapping
.circle — border-radius: 50%
.rounded-l — border-radius: 1rem
Cover image (fills parent container):
.font-semibold (font-family: var(--ff-semibold))
.u-caps (uppercase + letter-spacing, customise via --_caps-tracking)
.u-nowrap (white-space: nowrap)
.circle (border-radius: 50%)
.rounded-l (border-radius: var(--border-radius-l))
.u-cover-img (width/height 100%, object-fit: cover, display: block) Crossfade between two stacked children on hover. Useful for product thumbnails with an alternate-angle reveal.
Hover to reveal the second image.
<div class="u-img-swap">
<img src="front.jpg" alt="..." />
<img src="back.jpg" alt="..." />
</div>
Two loading indicators that reuse the shared spin keyframe.
Both respect prefers-reduced-motion.
| Class | Effect | Custom properties |
|---|---|---|
.u-spinner | Centered spinner overlay via ::after on any element (button, panel, …) | --_spinner-size (1.25rem), --_spinner-color (currentColor) |
.u-spinner-standalone | Solo spinner element for page/section loading | --_spinner-size (2rem), --_spinner-color (terracotta) |
<!-- On a button — hides label via color: transparent -->
<button class="u-button u-button--solid u-spinner" style="color: transparent;">
Loading
</button>
<!-- Standalone -->
<span class="u-spinner-standalone"></span> Centered anchor with flanking hairline rules. Used as a scroll bridge between sections.
<div class="u-lined-link">
<a href="#section-id">Link text ↓</a>
</div>
Composable hover behaviors. Combine multiple for layered effects.
All use consistent timing from --transition-speed tokens.
Image zoom on hover:
| Class | Effect |
|---|---|
.u-hover-lift | Subtle vertical rise (translateY -3px) |
.u-hover-shadow | Elevate with shadow |
.u-hover-rise | Lift + shadow combined |
.u-hover-zoom | Scale child image 1.03x |
.u-hover-arrow | Nudge arrow SVG right |
.u-hover-glow | Subtle background highlight |
.u-hover-color | Text color shift to CTA |
<!-- Compose multiple utilities -->
<a class="card u-hover-rise" href="...">
<div class="u-hover-zoom"><img src="..." /></div>
<span class="u-hover-arrow">Learn more →</span>
</a>
Scroll-reveal (.u-reveal, .u-reveal-up, .u-reveal-zoom)
and staggered entrance (.u-stagger, .u-stagger-fade) utilities are
documented with interactive demos in the Animations tab.
.u-reveal /* scroll: fade + scale */
.u-reveal-up /* scroll: fade + slide up */
.u-reveal-zoom /* scroll: child image zoom */
.u-stagger /* entrance: cascading fade-up (data-delay="1"–"6") */
.u-stagger-fade /* entrance: pure opacity fade-in */
Attention-grabbing shake for validation errors, locked elements, etc.
Respects prefers-reduced-motion.
.u-shake /* Horizontal shake animation, 0.5s ease */
/* Customise distance via --_shake-distance (default 4px) */ Show/hide elements based on viewport width. Breakpoint at 48em (768px).
.mobile-only — Only visible on mobile (<768px)
.desktop-only — Only visible on desktop (≥769px)
| Class | Effect |
|---|---|
.mobile-only | Hidden on desktop (≥769px) |
.desktop-only | Hidden on mobile (<768px) |
.hide-mobile | Same as .desktop-only (legacy) |
.hide-desktop | Same as .mobile-only (legacy) |
.visually-hidden
/* Hides content visually but keeps it accessible to screen readers */
Shared keyframes defined in global.css (outside @layer for global visibility).
Reuse these instead of defining new @keyframes. Customize via CSS custom properties.
These keyframes are available globally. Apply them with animation-name.
fade-in fade-up scale-up scale-in spin /* Available keyframes */
@keyframes fade-in /* opacity: 0 → 1 */
@keyframes fade-up /* opacity + translateY */
@keyframes scale-up /* opacity + scale */
@keyframes scale-in /* scale only (for zoom effects) */
@keyframes spin /* rotate 360° */
/* Customizable properties */
.my-element {
--_fade-distance: 1.5rem; /* fade-up travel distance */
--_scale-from: 0.98; /* scale-up starting scale */
--_scale-to: 1.03; /* scale-in ending scale */
}
Apply scroll-driven animations using animation-timeline: view().
Elements animate as they enter the viewport. Defined in Layout.astro with
is:global — scroll-driven animations break inside @layer.
Important: Use overflow: clip (not hidden) on
containers with .u-reveal-zoom. The hidden value creates a scroll
container that breaks the animation.
↓ Scroll the page to see these elements animate as they enter the viewport
.u-reveal — fade + scale.u-reveal-up — slide up.u-reveal-zoom — image zooms as it scrolls into view
<div class="u-reveal">Fades in with scale</div>
<div class="u-reveal-up">Slides up into view</div>
<div class="u-reveal-zoom">
<img src="..." alt="..." /> <!-- Image zooms as you scroll -->
</div> | Class | Effect | Animation Range |
|---|---|---|
.u-reveal | Fade + scale up | entry 0% → cover 35% |
.u-reveal-up | Fade + slide up | entry 0% → cover 35% |
.u-reveal-zoom | Child image scales | entry 0% → cover 60% |
The .u-hover-zoom utility scales child images on hover using
--transition-speed-zoom. Apply it to a container with overflow: clip
(set automatically by the utility).
img picture > img <!-- Hover zoom on image -->
<div class="u-hover-zoom">
<img src="..." alt="..." />
</div>
<!-- Works with <picture> too -->
<div class="u-hover-zoom">
<picture>
<img src="..." alt="..." />
</picture>
</div>
<!-- Combine with other hover utilities -->
<a class="card u-hover-rise" href="...">
<div class="u-hover-zoom"><img src="..." alt="..." /></div>
</a> | Property | Value |
|---|---|
| Scale factor | 1.03 |
| Transition speed | --transition-speed-zoom |
| Overflow | clip (set by utility) |
| Targets | > img, > video, > picture > img |
Cascading reveal for hero sections and page intros. Elements fade up one by one
using data-delay attributes. Use .u-stagger-fade for pure opacity (no translate).
<!-- Staggered entrance (hero pattern) -->
<p class="| u-stagger" data-delay="1">...</p>
<h1 class="| u-stagger" data-delay="2">...</h1>
<p class="| u-stagger" data-delay="3">...</p>
<!-- Pure fade for visuals -->
<div class="hero__visual | u-stagger-fade">
<img src="..." alt="..." />
</div> | Class | Effect | Delay steps |
|---|---|---|
.u-stagger | Fade + slide up (1rem) | data-delay="1"–"6" → 0.1s–0.6s |
.u-stagger-fade | Pure opacity fade-in | Fixed 0.3s delay |
Reusable <Spinner /> component for loading states.
Customize size and color via props. Respects prefers-reduced-motion.
Default Small / primary Large / CTA White on dark import Spinner from "@components/ui/Spinner.astro";
<!-- Default (2rem, terracotta) -->
<Spinner />
<!-- Custom size + color -->
<Spinner size="3rem" color="var(--clr-cta)" />
<!-- Small spinner for inline/button use -->
<Spinner size="1rem" color="white" />
<!-- Custom screen reader label -->
<Spinner label="Loading products…" /> | Prop | Type | Default | Description |
|---|---|---|---|
size | string | 2rem | Spinner diameter (any CSS length) |
color | string | terracotta | Color of the spinning arc |
label | string | "Loading…" | Screen reader announcement |
class | string | — | Additional CSS classes |
For JS-driven loading states, use the CSS utilities directly:
.u-spinner (pseudo-element overlay) or .u-spinner-standalone (element is the spinner).
Customize via --_spinner-size and --_spinner-color.
Combining keyframes with custom timing and properties.
/* Custom entry animation */
.my-element {
--_fade-distance: 2rem;
animation: fade-up 0.6s ease-out both;
}
/* Staggered children */
.my-list > * {
animation: fade-up 0.4s ease-out both;
}
.my-list > *:nth-child(1) { animation-delay: 0ms; }
.my-list > *:nth-child(2) { animation-delay: 100ms; }
.my-list > *:nth-child(3) { animation-delay: 200ms; }
Shared CSS blocks from 04-blocks.css. Reusable component patterns applied via class names.
Combine with utilities like .center-inline and .flow-space-* as needed.
Gold serif italic accent text for section headers.
The art of fine printing
<p class="section-subtitle | center-inline">The art of fine printing</p> | Property | Value |
|---|---|
| Font | Serif, italic |
| Size | var(--step-1) |
| Color | var(--clr-gold) |
White highlight box with soft shadow. Good for key statements or descriptions.
Experience museum-quality printing with our signature Acrylux process. Each piece is crafted by hand in our Belgian atelier.
<p class="callout-card | center-inline flow-space-s">
Key statement or description text here.
</p> | Property | Value |
|---|---|
| Font weight | var(--fw-semibold) |
| Line height | var(--lh-relaxed) |
| Background | White |
| Padding | 1.2em 1.8em |
| Border radius | var(--border-radius-l) |
| Shadow | var(--shadow-s) |
| Max width | 48rem |
Left-bordered highlight column, typically paired with text in a .column-layout.
Our signature process ensures each piece meets the highest standards of fine art printing.
<div class="column-layout" data-columns="40-60">
<div class="accent-border">
<h3>Heading</h3>
</div>
<div class="accent-border__text">
<p>Supporting text...</p>
</div>
</div> .popover-panel (in 04-blocks.css) styles floating panels for dropdowns
and menus. See them in action in the site header — language switcher, mega-menu, and cart.
Positioning uses CSS Anchor Positioning (Chrome/Edge) with a JS fallback in popover-panel.js
for Firefox/Safari. Details in styles/README.md → Popover Positioning.
Typical section header pattern using multiple blocks together.
Our Process
Where tradition meets innovation
Every print is hand-finished in our Belgian atelier, combining decades of expertise with cutting-edge technology.
<header class="text-center flow flow-space-xs">
<p class="u-label u-label--dashed">Eyebrow</p>
<h2>Section Title</h2>
<p class="section-subtitle | center-inline">Accent subtitle</p>
<p class="callout-card | center-inline flow-space-s">
Key statement in a highlight card.
</p>
</header>
SVG icons that inherit currentColor and scale with font-size.
Click any icon to copy its name.
Icons inherit font-size from their parent.
1rem 1.5rem 2rem 2.5rem 3rem ---
import Icon from "@components/ui/Icon.astro";
---
<Icon name="arrow-right" />
<!-- Scales with font-size -->
<span style="font-size: 2rem">
<Icon name="star" />
</span>
Top-layer UI for modals, drawers, and popovers. Built on native
<dialog> and Popover API for accessibility and light dismiss.
Native <dialog> element with two variants: Modal (centered)
and Drawer (slides from side). Supports sliding panes for multi-step flows.
| Prop | Values | Description |
|---|---|---|
variant | "modal" | "drawer" | Centered modal or side drawer |
position | "right" | "left" | Drawer slide direction |
title | string | Optional header title |
<!-- Trigger -->
<button data-open-dialog="my-modal">Open</button>
<!-- Modal -->
<DialogPanel id="my-modal" variant="modal" title="Hello">
<p>Content here</p>
<button data-close-dialog>Close</button>
</DialogPanel>
<!-- Drawer -->
<DialogPanel id="cart" variant="drawer" position="right" title="Cart">
...
</DialogPanel>
Multi-pane modals with sliding navigation for drill-down flows.
Each pane is a data-pane="name" div. History stack handles back navigation.
| Attribute | Description |
|---|---|
data-open-dialog="id" | Opens the dialog |
data-close-dialog | Closes the closest parent dialog |
data-slide-to="pane" | Slides to a named pane |
data-slide-back | Slides back to previous pane |
<DialogPanel id="example" variant="modal">
<div data-pane="main">
<h3>Choose an Option</h3>
<button data-slide-to="option-a">Option A →</button>
</div>
<div data-pane="option-a">
<div class="dialog-panel__pane-header">
<button class="dialog-panel__back" data-slide-back aria-label="Back">
<Icon name="chevron-left" />
</button>
<span>Option A Details</span>
</div>
<p>Content for option A...</p>
</div>
</DialogPanel> Native Popover API for dropdown menus and tooltips. Renders in the top layer with automatic light dismiss.
Positioning: Uses CSS Anchor Positioning in supported browsers (Chrome/Edge) so panels stick to their trigger through scroll and resize. Falls back to JS positioning in Firefox/Safari, with close-on-scroll to prevent drift.
Click-triggered popover
Click outside or press ESC to close.
Hover-triggered popover
Used for navigation dropdowns.
| Prop | Values | Description |
|---|---|---|
trigger | "click" | "hover" | How the popover opens. Click uses native popovertarget; hover uses JS. |
anchorIndex | number (optional) | Links to a trigger with anchor-name: --nav-anchor-{n} for CSS Anchor Positioning. |
| Trigger | Usage |
|---|---|
trigger="click" | Use popovertarget="id" on the button |
trigger="hover" | Use data-popover-hover="id" on the trigger element |
<!-- Click trigger -->
<button popovertarget="info">Info</button>
<PopoverPanel id="info" trigger="click">
Content here
</PopoverPanel>
<!-- Hover trigger with anchor positioning -->
<li style="anchor-name: --nav-anchor-0">
<button data-popover-hover="menu">Menu</button>
<PopoverPanel id="menu" trigger="hover" anchorIndex={0}>
Dropdown content
</PopoverPanel>
</li> Disclosure components for organizing content into switchable views or collapsible sections. Accessible with ARIA and keyboard navigation.
Accessible tabs with ARIA support and keyboard navigation.
Panels are child elements with IDs matching the pattern {id}-panel-{tabId}.
This is the overview content. First tab is active by default.
★★★★★ "Absolutely stunning quality!" — Customer
| Prop | Type | Description |
|---|---|---|
id | string | Unique identifier for the tab group |
tabs | Array | Array of { id, label } objects |
defaultTab | string | ID of initially active tab |
<Tabs id="product" tabs={[
{ id: "overview", label: "Overview" },
{ id: "specs", label: "Specifications" },
]}>
<section id="product-panel-overview" class="tabs__panel"
role="tabpanel" aria-labelledby="product-tab-overview">
Overview content
</section>
<section id="product-panel-specs" class="tabs__panel"
role="tabpanel" aria-labelledby="product-tab-specs" hidden>
Specs content
</section>
</Tabs>
Collapsible content using native <details> element.
Supports exclusive groups where only one item can be open at a time.
We use premium plexiglass and archival-quality printing materials that ensure your photos last a lifetime.
Standard shipping takes 5-7 business days. Express shipping options are available at checkout.
Yes, we offer a 30-day satisfaction guarantee. If you're not happy, contact us for a full refund.
Only one item can be open at a time within a group.
Opening this will close the others in this group.
Only one accordion in the group stays open.
Great for FAQ sections where you want focused reading.
| Prop | Type | Description |
|---|---|---|
open | boolean | Initially expanded |
group | string | Exclusive group name |
<!-- Single accordion -->
<Accordion open>
<span slot="summary">Question?</span>
<p>Answer here.</p>
</Accordion>
<!-- Exclusive group -->
<div data-accordion-group="faq">
<Accordion group="faq">
<span slot="summary">Q1</span>
<p>A1</p>
</Accordion>
<Accordion group="faq">
<span slot="summary">Q2</span>
<p>A2</p>
</Accordion>
</div> Flexible card with optional media, eyebrow, title, and footer slots. Three variants for different visual weights.
Flat Card
Default variant with no border or shadow.
Elevated Card
Subtle shadow for visual lift.
Outlined Card
Border for clear boundaries.
| Prop / Slot | Description |
|---|---|
variant | "flat" | "elevated" | "outlined" |
href | Makes entire card clickable |
slot="media" | Image or media at top |
slot="eyebrow" | Small label above title |
slot="title" | Card heading |
slot="footer" | Actions at bottom |
<Card variant="elevated">
<img slot="media" src="..." alt="..." />
<span slot="eyebrow">Category</span>
<span slot="title">Card Title</span>
<p>Card description text.</p>
<Button slot="footer" size="sm">Learn More</Button>
</Card> Infinite-loop carousel that auto-starts when scrolled into view. Two modes: discrete (snap to slides) or continuous (smooth constant motion). Pauses on hover/focus.
Smooth constant motion. Auto-starts when in view, pauses on hover.
| Prop | Type | Description |
|---|---|---|
id | string | Unique identifier |
navigation | boolean | Show prev/next buttons |
pagination | boolean | Show dot indicators |
autoplay | number (ms) | Discrete auto-advance interval |
continuous | boolean | Smooth constant scrolling |
speed | number | Pixels per frame (default: 1) |
loop | boolean | Infinite loop (default: true) |
gap | CSS value | Gap between slides |
<!-- Discrete with navigation -->
<Carousel id="gallery" navigation pagination>
<img src="1.jpg" alt="..." />
<img src="2.jpg" alt="..." />
</Carousel>
<!-- Continuous smooth scrolling -->
<Carousel id="marquee" continuous speed={0.5}>
<div>Item 1</div>
<div>Item 2</div>
</Carousel>
<!-- Discrete autoplay (4 seconds) -->
<Carousel id="hero" autoplay={4000} pagination>
...
</Carousel> Before/after image comparison slider with auto-animation and interactive hover. On desktop, the divider follows the cursor. On touch devices, it auto-animates.
Hover over the image to control the comparison divider.
| Prop | Type | Description |
|---|---|---|
srcBefore | string | URL/path for the "before" image (shown first) |
srcAfter | string | URL/path for the "after" image (revealed by slider) |
width | number | Image width for aspect ratio |
height | number | Image height for aspect ratio |
altBefore | string | Alt text for before image (default: "Before AI optimization") |
altAfter | string | Alt text for after image (default: "After AI optimization") |
resolvedUrls | boolean | If true, uses URLs directly. If false, appends .webp/.jpg extensions |
| Device | Behavior |
|---|---|
| Desktop (pointer: fine) | Auto-animates idle; divider follows cursor on hover |
| Touch (pointer: coarse) | Auto-animates continuously; handle hidden |
---
import AiImageCompare from "../components/AiImageCompare.astro";
---
<!-- With local images (auto .webp/.jpg) -->
<AiImageCompare
srcBefore="/images/photo-before"
srcAfter="/images/photo-after"
width={800}
height={600}
/>
<!-- With resolved URLs (external images) -->
<AiImageCompare
srcBefore="https://example.com/before.jpg"
srcAfter="https://example.com/after.jpg"
width={800}
height={600}
altBefore="Original photo"
altAfter="Enhanced photo"
resolvedUrls={true}
/> The component uses a smooth eased animation with pauses at endpoints.
/* Internal animation parameters */
SPEED: 0.00048 // Base sweep speed
PAUSE: 800ms // Pause at each end
HALF_LIFE: 80ms // Easing smoothness on hover
/* Cursor tracking uses exponential easing:
pos += diff * (1 - 0.5^(dt / HALF_LIFE))
This creates smooth, responsive following */ Interactive 360° product viewer. Users rotate the product by dragging, scrolling (desktop), or using arrow keys. Frames are preloaded on first interaction.
Drag left/right, scroll, or use arrow keys to rotate.
| Prop | Type | Description |
|---|---|---|
frames | string[] | Ordered array of frame image URLs |
alt | string | Accessible label for the viewer |
clamped | boolean | When true, frames stop at first/last instead of looping (user reverses to continue) |
speed | number | Drag sensitivity multiplier (default: 1, clamped default: 2) |
<!-- Standalone usage -->
<Viewer360 frames={frameUrls} alt="Product 360° view" />
<!-- In product gallery (via MDX frontmatter) -->
gallery:
- { src: "", label: "360°", is360: "acrylux-classic" }
- { src: "product-front.jpg", label: "Front" }
- { src: "product-side.jpg", label: "Side" } | Input | Action |
|---|---|
| Pointer drag (left/right) | Rotate through frames |
| Mouse wheel (desktop) | Step forward/backward one frame |
| Arrow keys (when focused) | Step forward/backward one frame |
| Touch drag (mobile) | Rotate through frames |
360/ ← parent album
Solo/ ← category album
acrylux-classic/ ← product album (= product slug)
acrylux-classic-01.jpg ← frame 1 of 36
acrylux-classic-02.jpg
...
acrylux-classic-36.jpg
acrylux-classic-thumbnail.jpg ← gallery thumbnail
Frames are sorted by filename. The thumbnail (*-thumbnail.*) is separated automatically.
At build time, get360Album(slug) finds the album by product slug anywhere under the "360" root.