Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Carousel

A scroll-snapping slideshow on Embla, with arrows, dots, and thumbnails.

Installation

pnpm dlx zazz-ui add carousel
npx zazz-ui add carousel
yarn dlx zazz-ui add carousel
bunx zazz-ui add carousel

Run zazz-ui init first if the project has no zazz/ folder yet.

Examples

<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

carousel attributes
AttributeKindValuesNotes
data-ui="carousel"Identityfree valueIdentity token in data-ui.
<ui-carousel>Tag formfree valueTag form; styled and, when scripted, registered under this name.
data-carousel-slotSlotcontainer dot dots next prev slide thumbs viewportSlot: a part inside the primitive.
data-carousel-stateStateactive dragging in-view snappedState the kit's script writes; read it in selectors, never author it.
data-carousel-initConfigfree valueConfiguration the kit's script reads.
data-carousel-keyboardConfigfree valueConfiguration the kit's script reads.
data-carousel-loopConfigfree valueConfiguration the kit's script reads.
data-carousel-pluginsConfigfree valueConfiguration the kit's script reads.
data-carousel-startConfigfree valueConfiguration the kit's script reads.
data-carousel-start-indexConfigfree valueConfiguration 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.

carousel hooks
HookDefaultState
--ui-carousel-cursor--grabbinggrabbinggrabbing
--ui-carousel-gap0px—
--ui-carousel-slide-basiscalc(100% - var(--ui-carousel-gap))—
--ui-carousel-slide-min-w0—
--ui-carousel-touch-actionpan-y pinch-zoom—