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
| Animation | Behavior |
|---|---|
slide-up | Starts below its final position; translates up into place. |
slide-down | Starts above; translates down. |
slide-left | Starts to the right; translates left. |
slide-right | Starts to the left; translates right. |
fade | Starts transparent; fades to opaque. No transform. |
grow | Starts slightly smaller (scale 0.9); grows to full size. |
shrink | Starts 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).
| Attribute | Default | What it controls |
|---|---|---|
data-zazz-duration | 900 (ms) | Transition duration. |
data-zazz-wait | 0 (ms) | Base delay before the animation starts. |
data-zazz-step | 80 (ms) | Stagger delay between children. data-zazz-each only. |
data-zazz-ease | cubic-bezier(0.4, 0, 0.2, 1) | Timing function. Accepts CSS keywords (ease-in-out, linear) or a cubic-bezier(...) value. |
data-zazz-distance | 4rem | Translation distance for slide animations. |
data-zazz-scale | 0.9 (grow) / 1.1 (shrink) | Override the initial scale for grow or shrink. |
data-zazz-order | natural | reversed flips the stagger order. data-zazz-each only. |
data-zazz-margin | 0px | IntersectionObserver rootMargin. Negative values delay the trigger; positive values fire it before the element fully enters the viewport. |
data-zazz-threshold | 0.2 | IntersectionObserver 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:
- Local CSS variable (e.g.,
--zazz-durationset inline). - Data attribute (e.g.,
data-zazz-duration="600"). --zazz-global-*set on:root.- CSS
@propertyinitial 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,
},
});| Method | Use 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:
- Reads the config and writes globals to
:root. - Queries every
[data-zazz]and configures one observer per unique threshold/margin combination. - Queries every
[data-zazz-each], applies per-child CSS variables (duration, ease, wait offset), and observes each child. - When an observed element crosses the viewport threshold, it adds the
.in-viewportclass 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
| Platform | Reference |
|---|---|
| Figma | Not applicable. Animations happen at runtime in the browser. |
| Webflow | Style 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 / Tailwind | Paste the <style> block into your stylesheet (or its imported equivalent) and load the <script> once. The library has no dependencies. |