Zazz Design Framework
Primitives

Motion

Scroll-triggered viewport entry animations driven by data attributes.

An animation isn't a script. It's an attribute.

Zazz Motion is a small library for animating elements as they enter the viewport. It's one stylesheet, one auto-initializing script, and seven animations. Attach a data-zazz attribute to an element, and the library handles the IntersectionObserver, the transition timing, and the reduced-motion fallback for you.

It exists because most marketing and ecommerce sites need scroll-reveal animations and don't need a 30kb animation library to do it.

Animations

AnimationBehavior
slide-upStarts below its final position; translates up into place.
slide-downStarts above; translates down.
slide-leftStarts to the right; translates left.
slide-rightStarts to the left; translates right.
fadeStarts transparent; fades to opaque. No transform.
growStarts slightly smaller (scale 0.9); grows to full size.
shrinkStarts slightly larger (scale 1.1); shrinks to full size.

All animations transition opacity from 0 to 1. Slide and scale animations also transition transform from a starting offset back to none.

Single element

For one-off reveals, add data-zazz="<animation>" to the element:

<h1 data-zazz="slide-up">Designed for design teams</h1>

<img data-zazz="fade" src="hero.jpg" alt="" />

<section data-zazz="slide-right" data-zazz-duration="600" data-zazz-wait="200">
  Section content.
</section>

The library walks every [data-zazz] on the page, sets up an IntersectionObserver, and applies the animation when the element scrolls into view. After it fires once, the observer disconnects.

Stagger groups

For lists, grids, or any sequence that should reveal in order, put data-zazz-each="<animation>" on a parent. The library animates the parent's direct children, offsetting each one by data-zazz-step milliseconds.

<ul data-zazz-each="slide-up" data-zazz-step="100">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
</ul>

Add data-zazz-order="reversed" to animate the last child first:

<ul data-zazz-each="fade" data-zazz-step="80" data-zazz-order="reversed">
  <li>First in markup, fourth to animate</li>
  <li>Second in markup, third to animate</li>
  <li>Third in markup, second to animate</li>
  <li>Fourth in markup, first to animate</li>
</ul>

Only direct children are animated. Nested elements are unaffected unless they have their own data-zazz or data-zazz-each attributes.

Configuration

Every animation behavior is tunable through data attributes on the animated element (for data-zazz) or the group parent (for data-zazz-each).

AttributeDefaultWhat it controls
data-zazz-duration900 (ms)Transition duration.
data-zazz-wait0 (ms)Base delay before the animation starts.
data-zazz-step80 (ms)Stagger delay between children. data-zazz-each only.
data-zazz-easecubic-bezier(0.4, 0, 0.2, 1)Timing function. Accepts CSS keywords (ease-in-out, linear) or a cubic-bezier(...) value.
data-zazz-distance4remTranslation distance for slide animations.
data-zazz-scale0.9 (grow) / 1.1 (shrink)Override the initial scale for grow or shrink.
data-zazz-ordernaturalreversed flips the stagger order. data-zazz-each only.
data-zazz-margin0pxIntersectionObserver rootMargin. Negative values delay the trigger; positive values fire it before the element fully enters the viewport.
data-zazz-threshold0.2IntersectionObserver threshold from 0 to 1. Higher values require more of the element to be visible before firing.
<div
  data-zazz="slide-up"
  data-zazz-duration="600"
  data-zazz-wait="200"
  data-zazz-ease="ease-out"
  data-zazz-distance="2rem"
  data-zazz-threshold="0.5"
>
  Bespoke reveal.
</div>

Global defaults

The defaults above are set on :root as CSS custom properties when the library initializes. Override them globally by re-declaring on :root (or any ancestor) before the script runs:

:root {
  --zazz-global-duration: 600ms;
  --zazz-global-ease: ease-out;
  --zazz-global-distance: 2rem;
  --zazz-global-grow: 0.85;
  --zazz-global-shrink: 1.15;
}

Per-element data attributes override these globals. The cascade is:

  1. Local CSS variable (e.g., --zazz-duration set inline).
  2. Data attribute (e.g., data-zazz-duration="600").
  3. --zazz-global-* set on :root.
  4. CSS @property initial value declared in the library.

Most projects set globals once and use data attributes for the few sections that need bespoke timing.

JavaScript API

The library auto-initializes on DOMContentLoaded. The default config is enough for most cases. If you need custom config, instantiate manually before the auto-init runs:

ZazzMotion.disableAutoInit();

const motion = new ZazzMotion({
  config: {
    duration: 600,
    ease: 'ease-in-out',
    threshold: 0.3,
    margin: '100px',
    step: 100,
  },
});
MethodUse for
new ZazzMotion(options)Initialize with custom config.
motion.refresh()Re-scan the DOM and rewire observers. Call after dynamically inserting data-zazz markup.
ZazzMotion.disableAutoInit()Prevent the auto-init on DOMContentLoaded so you can instantiate manually.
ZazzMotion.getAutoInstance()Returns the auto-init instance, or null if disabled.

Refresh is the important one. The library wires observers when it loads, not continuously. If you load content via fetch or change the DOM in a way that adds new data-zazz elements, call motion.refresh() so they pick up the animations.

Reduced motion

When the user's OS reports prefers-reduced-motion: reduce, the library disables itself entirely:

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

Elements show at their final state immediately. No transition runs.

This is not a configurable behavior. Respecting reduced motion is built into the stylesheet at the @media level, so even users who never load the JavaScript get the right experience.

What the JS does

When it initializes:

  1. Reads the config and writes globals to :root.
  2. Queries every [data-zazz] and configures one observer per unique threshold/margin combination.
  3. Queries every [data-zazz-each], applies per-child CSS variables (duration, ease, wait offset), and observes each child.
  4. When an observed element crosses the viewport threshold, it adds the .in-viewport class and unobserves the element.

The .in-viewport class is what triggers the transition to the final state. Until it's there, the element sits at its starting transform and opacity.

Cross-platform

PlatformReference
FigmaNot applicable. Animations happen at runtime in the browser.
WebflowStyle block goes in global-head__custom; script goes in global-end__motion (or anywhere on the page before </body>). Auto-initializes once both are present.
CSS / TailwindPaste the <style> block into your stylesheet (or its imported equivalent) and load the <script> once. The library has no dependencies.

Where to next

On this page