Skip to content

Type to search the docs, API, and blog.

↑↓ navigate↵ openesc close
Zazz 0.7 alpha
API navigation

API

Reveal

Scroll-driven reveal animations through `data-reveal` attributes.

Installation

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

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

Examples

Arrives first
Then this one
Then the last
<div
  data-reveal-each="slide-up"
  data-reveal-step="100"
  style="
    --display: grid;
    --grid-cols: 1;
    --grid-cols--sm: 3;
    --gap: 4;
    --w: 100%;
  "
>
  <div data-ui="card" data-card-variant="muted" style="--p: 6">Arrives first</div>
  <div data-ui="card" data-card-variant="muted" style="--p: 6">Then this one</div>
  <div data-ui="card" data-card-variant="muted" style="--p: 6">Then the last</div>
</div>
/**
 * reveal.css — Scroll reveals ([data-reveal] / [data-reveal-each]; the script marks
 *              revealed targets data-reveal-state="in-view")
 *
 * @layer      variables, utilities
 * @requires   _layers.css, _variables.css
 * @uses       prefers-reduced-motion — native media feature
 * @uses       sibling-index(), sibling-count() — native stagger delays (@supports
 *             gated; where unsupported, reveal.js writes a per-child
 *             --ui-reveal-wait instead)
 * @uses       Behaviour requires zazz/scripts/reveal.js (IntersectionObserver)
 * @tokens     --ui-reveal-global-* (@layer variables);
 *             per-instance --ui-reveal-* set from data-* attributes by reveal.js;
 *             --ui-reveal-stagger-base / --ui-reveal-stagger-step written on stagger
 *             groups by reveal.js for the native sibling-index() path
 *
 * A lightweight, configurable animation system for scroll-based reveals. Focuses
 * on viewport entry animations for single elements and direct children of stagger
 * groups. Available animations: slide-up, slide-down, slide-left, slide-right,
 * fade, grow, shrink.
 *
 * @example  Single element
 *   <div data-reveal="slide-up">Content</div>
 *
 * @example  Staggered children — animation applied to each direct child
 *   <div data-reveal-each="fade" data-reveal-step="100">
 *     <div>Item 1</div>
 *     <p>Item 2</p>
 *   </div>
 *
 * Data attributes (on the element with data-reveal or data-reveal-each):
 *   data-reveal="animation-name"        single elements
 *   data-reveal-each="animation-name"   groups; applied to children
 *   data-reveal-duration="300"          ms
 *   data-reveal-wait="0"                ms, base delay
 *   data-reveal-step="80"               ms, stagger delay for groups
 *   data-reveal-ease="ease-in-out"
 *   data-reveal-distance="2rem"         translation distance
 *   data-reveal-order="reversed"        stagger order for groups
 *   data-reveal-margin="0px"            IntersectionObserver rootMargin
 *   data-reveal-threshold="0.2"         IntersectionObserver threshold
 *   data-reveal-scale="0.5"             optional initial scale (requires JS mod)
 */

@layer variables {
  :root {
    --ui-reveal-global-duration: var(--default-transition-duration);
    --ui-reveal-global-timing-function: var(--default-transition-timing-function);
    --ui-reveal-global-wait: 0ms;
    --ui-reveal-global-distance: 1rem;
    --ui-reveal-global-grow: 0.97;
    --ui-reveal-global-shrink: 1.03;
  }
}

@layer ui.utilities {
  @media screen and (prefers-reduced-motion: no-preference) {
    /* Effective values + hidden state, at :where() specificity so the in-view
       final state overrides it. */
    :where(
      [data-reveal]:not([data-gjs-type]),
      [data-reveal-each]:not([data-gjs-type]) > *:not([data-gjs-type])
    ) {
      --ui-reveal-effective-duration: var(--ui-reveal-duration, var(--ui-reveal-global-duration));
      --ui-reveal-effective-ease: var(--ui-reveal-ease, var(--ui-reveal-global-timing-function));
      --ui-reveal-effective-wait: var(--ui-reveal-wait, var(--ui-reveal-global-wait));
      --ui-reveal-effective-distance: var(--ui-reveal-distance, var(--ui-reveal-global-distance));
      will-change: opacity, transform;
      opacity: 0;
    }

    /* Deliberately not wrapped in :where(): its (0,1,0) specificity beats bare
       transition utilities in this layer that would reset transition-delay and
       wipe out the per-child stagger. The gjs exclusion uses :not(:where()) to
       add no specificity, so the fade override below still wins on order. */
    [data-reveal]:not(:where([data-gjs-type])),
    [data-reveal-each]:not(:where([data-gjs-type])) > *:not(:where([data-gjs-type])) {
      transition-timing-function: var(--ui-reveal-effective-ease);
      transition-duration: var(--ui-reveal-effective-duration);
      transition-delay: var(--ui-reveal-effective-wait);
      transition-property: opacity, transform;
    }

    /* Native stagger: wait = base + step × (index − 1); reversed groups count
       from the end. reveal.js writes the base/step on the group and skips its
       per-child --ui-reveal-wait fallback here. The literal 80ms mirrors the JS
       default step. */
    @supports (top: calc(sibling-index() * 1px)) {
      [data-reveal-each] > * {
        --ui-reveal-wait: calc(
          var(--ui-reveal-stagger-base, var(--ui-reveal-global-wait)) +
            var(--ui-reveal-stagger-step, 80ms) * (sibling-index() - 1)
        );
      }

      [data-reveal-each][data-reveal-order="reversed"] > * {
        --ui-reveal-wait: calc(
          var(--ui-reveal-stagger-base, var(--ui-reveal-global-wait)) +
            var(--ui-reveal-stagger-step, 80ms) * (sibling-count() - sibling-index())
        );
      }
    }

    [data-reveal="slide-up"]:not(:where([data-reveal-state~="in-view"], [data-gjs-type])),
    [data-reveal-each="slide-up"] > *:not(:where([data-reveal-state~="in-view"], [data-gjs-type])) {
      transform: translateY(var(--ui-reveal-effective-distance));
    }

    [data-reveal="slide-down"]:not(:where([data-reveal-state~="in-view"], [data-gjs-type])),
    [data-reveal-each="slide-down"]
      > *:not(:where([data-reveal-state~="in-view"], [data-gjs-type])) {
      transform: translateY(calc(-1 * var(--ui-reveal-effective-distance)));
    }

    [data-reveal="slide-left"]:not(:where([data-reveal-state~="in-view"], [data-gjs-type])),
    [data-reveal-each="slide-left"]
      > *:not(:where([data-reveal-state~="in-view"], [data-gjs-type])) {
      transform: translateX(var(--ui-reveal-effective-distance));
    }

    [data-reveal="slide-right"]:not(:where([data-reveal-state~="in-view"], [data-gjs-type])),
    [data-reveal-each="slide-right"]
      > *:not(:where([data-reveal-state~="in-view"], [data-gjs-type])) {
      transform: translateX(calc(-1 * var(--ui-reveal-effective-distance)));
    }

    [data-reveal="fade"]:not(:where([data-reveal-state~="in-view"], [data-gjs-type])),
    [data-reveal-each="fade"] > *:not(:where([data-reveal-state~="in-view"], [data-gjs-type])) {
      transition-property: opacity;
      transform: none;
    }

    [data-reveal="grow"]:not(:where([data-reveal-state~="in-view"], [data-gjs-type])),
    [data-reveal-each="grow"] > *:not(:where([data-reveal-state~="in-view"], [data-gjs-type])) {
      transform: scale(var(--ui-reveal-scale, var(--ui-reveal-global-grow)));
    }

    [data-reveal="shrink"]:not(:where([data-reveal-state~="in-view"], [data-gjs-type])),
    [data-reveal-each="shrink"] > *:not(:where([data-reveal-state~="in-view"], [data-gjs-type])) {
      transform: scale(var(--ui-reveal-scale, var(--ui-reveal-global-shrink)));
    }

    [data-reveal]:where([data-reveal-state~="in-view"], [data-gjs-type]),
    [data-reveal-each] > *:where([data-reveal-state~="in-view"], [data-gjs-type]) {
      opacity: 1;
      transform: none;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    :where([data-reveal], [data-reveal-each] > *) {
      transition: none !important;
      transform: none !important;
      opacity: 1 !important;
      will-change: auto !important;
    }
  }
}

Attributes

reveal attributes
AttributeKindValuesNotes
data-reveal-eachPresetfade grow shrink slide-down slide-left slide-right slide-upPreset: a named bundle of values.
data-reveal-orderPresetreversedPreset: a named bundle of values.
data-reveal-stateStatein-viewState the kit's script writes; read it in selectors, never author it.
data-revealConfigfade grow shrink slide-down slide-left slide-right slide-upConfiguration the kit's script reads.
data-reveal-distanceConfigfree valueConfiguration the kit's script reads.
data-reveal-durationConfigfree valueConfiguration the kit's script reads.
data-reveal-easeConfigfree valueConfiguration the kit's script reads.
data-reveal-marginConfigfree valueConfiguration the kit's script reads.
data-reveal-scaleConfigfree valueConfiguration the kit's script reads.
data-reveal-stepConfigfree valueConfiguration the kit's script reads.
data-reveal-thresholdConfigfree valueConfiguration the kit's script reads.
data-reveal-waitConfigfree 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.

reveal hooks
HookDefaultState
--ui-reveal-effective-distancevar(--ui-reveal-distance, var(--ui-reveal-global-distance))—
--ui-reveal-effective-durationvar(--ui-reveal-duration, var(--ui-reveal-global-duration))—
--ui-reveal-effective-easevar(--ui-reveal-ease, var(--ui-reveal-global-timing-function))—
--ui-reveal-effective-waitvar(--ui-reveal-wait, var(--ui-reveal-global-wait))—
--ui-reveal-global-distance1rem—
--ui-reveal-global-durationvar(--default-transition-duration)—
--ui-reveal-global-grow0.97—
--ui-reveal-global-shrink1.03—
--ui-reveal-global-timing-functionvar(--default-transition-timing-function)—
--ui-reveal-global-wait0ms—
--ui-reveal-waitcalc( var(--ui-reveal-stagger-base, var(--ui-reveal-global-wait)) + var(--ui-reveal-stagger-step, 80ms) * (sibling-index() - 1) )—