API
Reveal
Scroll-driven reveal animations through `data-reveal` attributes.
Installation
pnpm dlx zazz-ui add revealnpx zazz-ui add revealyarn dlx zazz-ui add revealbunx zazz-ui add revealRun 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
| Attribute | Kind | Values | Notes |
|---|---|---|---|
data-reveal-each | Preset | fade grow shrink slide-down slide-left slide-right slide-up | Preset: a named bundle of values. |
data-reveal-order | Preset | reversed | Preset: a named bundle of values. |
data-reveal-state | State | in-view | State the kit's script writes; read it in selectors, never author it. |
data-reveal | Config | fade grow shrink slide-down slide-left slide-right slide-up | Configuration the kit's script reads. |
data-reveal-distance | Config | free value | Configuration the kit's script reads. |
data-reveal-duration | Config | free value | Configuration the kit's script reads. |
data-reveal-ease | Config | free value | Configuration the kit's script reads. |
data-reveal-margin | Config | free value | Configuration the kit's script reads. |
data-reveal-scale | Config | free value | Configuration the kit's script reads. |
data-reveal-step | Config | free value | Configuration the kit's script reads. |
data-reveal-threshold | Config | free value | Configuration the kit's script reads. |
data-reveal-wait | 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-reveal-effective-distance | var(--ui-reveal-distance, var(--ui-reveal-global-distance)) | — |
--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-global-distance | 1rem | — |
--ui-reveal-global-duration | var(--default-transition-duration) | — |
--ui-reveal-global-grow | 0.97 | — |
--ui-reveal-global-shrink | 1.03 | — |
--ui-reveal-global-timing-function | var(--default-transition-timing-function) | — |
--ui-reveal-global-wait | 0ms | — |
--ui-reveal-wait | calc( var(--ui-reveal-stagger-base, var(--ui-reveal-global-wait)) + var(--ui-reveal-stagger-step, 80ms) * (sibling-index() - 1) ) | — |