API
Carousel
A scroll-snapping slideshow on Embla, with arrows, dots, and thumbnails.
Installation
pnpm dlx zazz-ui add carouselnpx zazz-ui add carouselyarn dlx zazz-ui add carouselbunx zazz-ui add carouselRun zazz-ui init first if the project has no zazz/ folder yet.
Examples
Featured Articles
How quickly zebras jump
Striped coats and sudden bursts of speed help herds evade predators when the grasslands go quiet at dusk.
Lions at golden hour
Warm light and long shadows make dawn patrols the best time to photograph big cats on the open plain.
Migration patterns decoded
Satellite collars reveal ancient routes herds still follow each dry season across the floodplain.
Rewilding the highlands
Community nurseries are planting native grasses and bringing endemic songbirds back to the ridgeline.
<ui-carousel>
<div
style="
--display: flex;
--flex-wrap: wrap;
--items: end;
--justify: space-between;
--pb: 11;
--gap: 6;
"
>
<h2
style="
--font-size: var(--font-size-h5);
--font-weight: heading;
--font-family: var(--font-family-heading);
--leading: var(--leading-h5);
"
>
Featured Articles
</h2>
<div
style="
--display: flex;
--display--md: none;
--gap: 2;
"
>
<button
type="button"
data-ui="button"
data-button-size="icon"
data-button-variant="muted"
data-carousel-slot="prev"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
aria-hidden="true"
style="--size: 4"
>
<rect width="256" height="256" fill="none" />
<polyline
points="160 208 80 128 160 48"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="14"
/>
</svg>
</button>
<button
type="button"
data-ui="button"
data-button-size="icon"
data-button-variant="muted"
data-carousel-slot="next"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
aria-hidden="true"
style="--size: 4"
>
<rect width="256" height="256" fill="none" />
<polyline
points="96 48 176 128 96 208"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="14"
/>
</svg>
</button>
</div>
</div>
<div data-carousel-slot="viewport" style="--overflow: visible">
<div data-carousel-slot="container" style="--ml: -6">
<a
data-carousel-slot="slide"
href="#"
style="
--basis: 100%;
--basis--sm: 50%;
--basis--md: 33.333%;
--basis--lg: 25%;
--cursor: pointer;
"
>
<div style="--pl: 6">
<div
style="
--display: grid;
--gap: 4;
--text: var(--color-card-foreground);
--bg: var(--color-card);
--shadow: sm;
--rounded: var(--radius-md);
--overflow: clip;
"
>
<figure
style="
--aspect: 3 / 2;
--rounded: var(--radius-md) var(--radius-md) 0 0;
--overflow: clip;
"
>
<img
width="300"
height="200"
loading="lazy"
src="https://images.unsplash.com/photo-1500382017468-9049fed747ef?auto=format&fit=crop&w=300&h=200&q=60"
alt="Zebras grazing at dawn on the savanna"
style="
--size: 100%;
--opacity: 1;
--opacity--hover: 0.75;
--scale: 1;
--scale--hover: 1.02;
--transition: var(--default-transition);
--object-fit: cover;
"
/>
</figure>
<div
style="
--display: grid;
--px: 4;
--pb: 4;
--gap: 4;
"
>
<header
style="
--display: grid;
--gap: 2;
"
>
<div
style="
--display: flex;
--items: center;
--gap: 2;
"
>
<span data-ui="badge" data-badge-variant="muted">Field notes</span>
</div>
<hgroup
style="
--display: grid;
--gap: 2;
"
>
<h3
style="
--font-size: var(--font-size-lg);
--font-family: var(--font-family-heading);
--leading: var(--leading-lg);
--tracking: var(--tracking-lg);
--line-clamp: 1;
"
>
How quickly zebras jump
</h3>
<p
style="
--font-size: var(--font-size-sm);
--leading: var(--leading-sm);
--tracking: var(--tracking-sm);
--line-clamp: 2;
--text: var(--color-muted-foreground);
"
>
Striped coats and sudden bursts of speed help herds evade predators when the
grasslands go quiet at dusk.
</p>
</hgroup>
</header>
<footer
style="
--display: grid;
--justify: end;
--pt: 4;
--pb: 2;
--gap: 4;
--border-t: 1;
"
>
<div data-ui="button" data-button-variant="link">
Read article
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<line
x1="40"
y1="128"
x2="216"
y2="128"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<polyline
points="144 56 216 128 144 200"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</div>
</footer>
</div>
</div>
</div>
</a>
<a
data-carousel-slot="slide"
href="#"
style="
--basis: 100%;
--basis--sm: 50%;
--basis--md: 33.333%;
--basis--lg: 25%;
--cursor: pointer;
"
>
<div style="--pl: 6">
<div
style="
--display: grid;
--gap: 4;
--text: var(--color-card-foreground);
--bg: var(--color-card);
--shadow: sm;
--rounded: var(--radius-md);
--overflow: clip;
"
>
<figure
style="
--aspect: 3 / 2;
--rounded: var(--radius-md) var(--radius-md) 0 0;
--overflow: clip;
"
>
<img
width="300"
height="200"
loading="lazy"
src="https://images.unsplash.com/photo-1564760055775-d63b17a55c44?auto=format&fit=crop&w=300&h=200&q=60"
alt="A lion resting in tall golden grass"
style="
--size: 100%;
--opacity: 1;
--opacity--hover: 0.75;
--scale: 1;
--scale--hover: 1.02;
--transition: var(--default-transition);
--object-fit: cover;
"
/>
</figure>
<div
style="
--display: grid;
--px: 4;
--pb: 4;
--gap: 4;
"
>
<header
style="
--display: grid;
--gap: 2;
"
>
<div
style="
--display: flex;
--items: center;
--gap: 2;
"
>
<span data-ui="badge" data-badge-variant="muted">Wildlife</span>
</div>
<hgroup
style="
--display: grid;
--gap: 2;
"
>
<h3
style="
--font-size: var(--font-size-lg);
--font-family: var(--font-family-heading);
--leading: var(--leading-lg);
--tracking: var(--tracking-lg);
--line-clamp: 1;
"
>
Lions at golden hour
</h3>
<p
style="
--font-size: var(--font-size-sm);
--leading: var(--leading-sm);
--tracking: var(--tracking-sm);
--line-clamp: 2;
--text: var(--color-muted-foreground);
"
>
Warm light and long shadows make dawn patrols the best time to photograph big
cats on the open plain.
</p>
</hgroup>
</header>
<footer
style="
--display: grid;
--justify: end;
--pt: 4;
--pb: 2;
--gap: 4;
--border-t: 1;
"
>
<div data-ui="button" data-button-variant="link">
View gallery
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<line
x1="40"
y1="128"
x2="216"
y2="128"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<polyline
points="144 56 216 128 144 200"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</div>
</footer>
</div>
</div>
</div>
</a>
<a
data-carousel-slot="slide"
href="#"
style="
--basis: 100%;
--basis--sm: 50%;
--basis--md: 33.333%;
--basis--lg: 25%;
--cursor: pointer;
"
>
<div style="--pl: 6">
<div
style="
--display: grid;
--gap: 4;
--text: var(--color-card-foreground);
--bg: var(--color-card);
--shadow: sm;
--rounded: var(--radius-md);
--overflow: clip;
"
>
<figure
style="
--aspect: 3 / 2;
--rounded: var(--radius-md) var(--radius-md) 0 0;
--overflow: clip;
"
>
<img
width="300"
height="200"
loading="lazy"
src="https://images.unsplash.com/photo-1511735111819-9a3f7709049c?auto=format&fit=crop&w=300&h=200&q=60"
alt="Elephants crossing a dusty riverbed"
style="
--size: 100%;
--opacity: 1;
--opacity--hover: 0.75;
--scale: 1;
--scale--hover: 1.02;
--transition: var(--default-transition);
--object-fit: cover;
"
/>
</figure>
<div
style="
--display: grid;
--px: 4;
--pb: 4;
--gap: 4;
"
>
<header
style="
--display: grid;
--gap: 2;
"
>
<div
style="
--display: flex;
--items: center;
--gap: 2;
"
>
<span data-ui="badge" data-badge-variant="muted">Research</span>
</div>
<hgroup
style="
--display: grid;
--gap: 2;
"
>
<h3
style="
--font-size: var(--font-size-lg);
--font-family: var(--font-family-heading);
--leading: var(--leading-lg);
--tracking: var(--tracking-lg);
--line-clamp: 1;
"
>
Migration patterns decoded
</h3>
<p
style="
--font-size: var(--font-size-sm);
--leading: var(--leading-sm);
--tracking: var(--tracking-sm);
--line-clamp: 2;
--text: var(--color-muted-foreground);
"
>
Satellite collars reveal ancient routes herds still follow each dry season
across the floodplain.
</p>
</hgroup>
</header>
<footer
style="
--display: grid;
--justify: end;
--pt: 4;
--pb: 2;
--gap: 4;
--border-t: 1;
"
>
<div data-ui="button" data-button-variant="link">
See the data
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<line
x1="40"
y1="128"
x2="216"
y2="128"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<polyline
points="144 56 216 128 144 200"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</div>
</footer>
</div>
</div>
</div>
</a>
<a
data-carousel-slot="slide"
href="#"
style="
--basis: 100%;
--basis--sm: 50%;
--basis--md: 33.333%;
--basis--lg: 25%;
--cursor: pointer;
"
>
<div style="--pl: 6">
<div
style="
--display: grid;
--gap: 4;
--text: var(--color-card-foreground);
--bg: var(--color-card);
--shadow: sm;
--rounded: var(--radius-md);
--overflow: clip;
"
>
<figure
style="
--aspect: 3 / 2;
--rounded: var(--radius-md) var(--radius-md) 0 0;
--overflow: clip;
"
>
<img
width="300"
height="200"
loading="lazy"
src="https://images.unsplash.com/photo-1470071459604-3b5ec3a7fe05?auto=format&fit=crop&w=300&h=200&q=60"
alt="Misty highland valley at sunrise"
style="
--size: 100%;
--opacity: 1;
--opacity--hover: 0.75;
--scale: 1;
--scale--hover: 1.02;
--transition: var(--default-transition);
--object-fit: cover;
"
/>
</figure>
<div
style="
--display: grid;
--px: 4;
--pb: 4;
--gap: 4;
"
>
<header
style="
--display: grid;
--gap: 2;
"
>
<div
style="
--display: flex;
--items: center;
--gap: 2;
"
>
<span data-ui="badge" data-badge-variant="muted">Conservation</span>
</div>
<hgroup
style="
--display: grid;
--gap: 2;
"
>
<h3
style="
--font-size: var(--font-size-lg);
--font-family: var(--font-family-heading);
--leading: var(--leading-lg);
--tracking: var(--tracking-lg);
--line-clamp: 1;
"
>
Rewilding the highlands
</h3>
<p
style="
--font-size: var(--font-size-sm);
--leading: var(--leading-sm);
--tracking: var(--tracking-sm);
--line-clamp: 2;
--text: var(--color-muted-foreground);
"
>
Community nurseries are planting native grasses and bringing endemic songbirds
back to the ridgeline.
</p>
</hgroup>
</header>
<footer
style="
--display: grid;
--justify: end;
--pt: 4;
--pb: 2;
--gap: 4;
--border-t: 1;
"
>
<div data-ui="button" data-button-variant="link">
Get started
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256">
<rect width="256" height="256" fill="none" />
<line
x1="40"
y1="128"
x2="216"
y2="128"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
<polyline
points="144 56 216 128 144 200"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="16"
/>
</svg>
</div>
</footer>
</div>
</div>
</div>
</a>
</div>
</div>
</ui-carousel>/**
* carousel.css — Embla carousel (ui-carousel | [data-ui~="carousel"] + data-carousel-slot="carousel-*" hooks)
*
* @layer variables, components
* @requires _layers.css, _variables.css
* @uses data-carousel attributes — layout hooks only; behaviour from zazz/scripts/embla.js
* @uses container-type on viewport — basis-* / @sm:basis-* utils query [data-carousel-slot~="viewport"] width
* @tokens --ui-carousel-* (@layer variables)
* @consumedby lightbox.css
*/
@layer variables {
:root {
/* the gap between slides; a --gap utility on the container wins (dual-mode) */
--ui-carousel-gap: 0px;
--ui-carousel-slide-basis: calc(100% - var(--ui-carousel-gap));
--ui-carousel-slide-min-w: 0;
--ui-carousel-touch-action: pan-y pinch-zoom;
--ui-carousel-cursor--grabbing: grabbing;
}
}
@layer ui.components {
/* custom elements are inline by default */
ui-carousel {
display: block;
}
:is(
body:has(
:where(ui-carousel, [data-ui~="carousel"])
[data-carousel-slot~="viewport"][data-carousel-state~="dragging"]
)
*
) {
cursor: var(--ui-carousel-cursor--grabbing);
}
:where(ui-carousel, [data-ui~="carousel"]) {
position: relative;
isolation: isolate;
}
:where(ui-carousel, [data-ui~="carousel"]) [data-carousel-slot~="viewport"] {
overflow: clip;
max-block-size: 100%;
max-inline-size: 100%;
}
:where(ui-carousel, [data-ui~="carousel"]) [data-carousel-slot~="container"] {
display: flex;
--_gap-len: var(--_gap-resolved, var(--ui-carousel-gap));
--_gap-num: var(--_gap-resolved, var(--ui-carousel-gap));
gap: calc(var(--_gap-len) + var(--_gap-num) * var(--spacing));
touch-action: var(--ui-carousel-touch-action);
}
:where(ui-carousel, [data-ui~="carousel"]) [data-carousel-slot~="slide"] {
flex: 0 0 var(--ui-carousel-slide-basis);
min-inline-size: var(--ui-carousel-slide-min-w);
/* Embla loops by moving slides with transform; a transition on it (a link's
default, say) sweeps a slide across the viewport. Animate a slide's child. */
transition: none;
}
:where(ui-carousel, [data-ui~="carousel"])
[data-carousel-slot~="thumbs"]
[data-carousel-slot~="slide"] {
flex: 0 0 auto;
}
}// primitives/carousel/carousel.js
"use strict";
import { EmblaInit } from "../../base/embla.js";
import { ZazzElement, defineZazzElement } from "../../base/zazz-element.js";
class UiCarouselElement extends ZazzElement {
setup(signal) {
const dialog = this.closest("dialog");
if (dialog && !dialog.open) {
// Closed dialogs are display:none — Embla can't measure the viewport.
// Initialize on the dialog's open instead (zazz:dialog-open).
dialog.addEventListener("zazz:dialog-open", () => this.init(), { signal });
return;
}
this.init();
}
teardown() {
// Abort first so the per-carousel DOM listeners (prev/next, dots, thumbs,
// drag-click suppression) are removed before the Embla instances are torn down.
this._emblaController?.abort();
delete this._emblaController;
this._emblaApi?.destroy();
this._emblaApiThumb?.destroy();
delete this._emblaApi;
delete this._emblaApiThumb;
// Allow re-initialization if the element is re-inserted.
this.removeAttribute("data-carousel-init");
}
/**
* @description Initializes the carousel. Idempotent — already-initialized
* roots and roots inside closed dialogs are skipped by `initRoot`.
*/
init() {
EmblaInit.initRoot(this);
}
/**
* @returns The Embla API, or null before initialization.
*/
get api() {
return this._emblaApi ?? null;
}
}
defineZazzElement("ui-carousel", UiCarouselElement);
export { UiCarouselElement };Behavior
primitives/carousel/carousel.js
<ui-carousel> — HTML web component for carousels. Light-DOM custom element that wraps standard carousel markup and owns the Embla lifecycle: it initializes on connect (via EmblaInit.initRoot) and destroys its instances on disconnect, so dynamically inserted or SPA-swapped carousels need no manual wiring.
The element *is* the carousel root — CSS and embla.js target :is(ui-carousel, [data-ui~="carousel"]) directly, and data-carousel-* configuration attributes are read off the element (see embla.js for the full attribute reference). No shadow DOM; children are regular markup.
Carousels inside a closed <dialog> defer initialization until the dialog first opens (a closed dialog is display: none, so Embla cannot measure the viewport).
Events: zazz:dialog-open
Attributes
| Attribute | Kind | Values | Notes |
|---|---|---|---|
data-ui="carousel" | Identity | free value | Identity token in data-ui. |
<ui-carousel> | Tag form | free value | Tag form; styled and, when scripted, registered under this name. |
data-carousel-slot | Slot | container dot dots next prev slide thumbs viewport | Slot: a part inside the primitive. |
data-carousel-state | State | active dragging in-view snapped | State the kit's script writes; read it in selectors, never author it. |
data-carousel-init | Config | free value | Configuration the kit's script reads. |
data-carousel-keyboard | Config | free value | Configuration the kit's script reads. |
data-carousel-loop | Config | free value | Configuration the kit's script reads. |
data-carousel-plugins | Config | free value | Configuration the kit's script reads. |
data-carousel-start | Config | free value | Configuration the kit's script reads. |
data-carousel-start-index | Config | free value | Configuration the kit's script reads. |
Hooks
Set a hook on :root, on a subtree, or inline on one element. A state hook (--…--hover) themes that state only; the primitive keeps its other states.
| Hook | Default | State |
|---|---|---|
--ui-carousel-cursor--grabbing | grabbing | grabbing |
--ui-carousel-gap | 0px | — |
--ui-carousel-slide-basis | calc(100% - var(--ui-carousel-gap)) | — |
--ui-carousel-slide-min-w | 0 | — |
--ui-carousel-touch-action | pan-y pinch-zoom | — |