motion-tokens
v0.3.6
Published
Token-based GSAP scroll animation system. Compose any animation with a simple string: fade-up, blur-left-slow-scrub, words-fast, chars-up.
Maintainers
Readme
motion-tokens
Declarative scroll animations for the modern web. Compose any GSAP animation with a single data-animate attribute — no per-element JavaScript, no configuration files, no boilerplate.
<div data-animate="fade-up">Fades in from below</div>
<div data-animate="blur-left-slow">Blurs in from the left, slowly</div>
<p data-animate="words-up-bounce">Word-by-word reveal with bounce easing</p>
<div data-animate="fade|blur-up-spring">Compound fade + blur with spring ease</div>
<div data-animate="clip-left">Clip-path reveal from left</div>Why motion-tokens?
Most animation libraries force you to choose: write verbose JavaScript for every element, or accept a limited set of presets. motion-tokens gives you a composable token grammar that covers the gap. You get hundreds of animation combinations from a handful of memorizable tokens, all driven from your HTML.
- Zero JavaScript per element — just add
data-animateand you're done - Composable grammar — mix effects, directions, speeds, and eases freely
- Automatic scroll triggering — powered by GSAP ScrollTrigger under the hood
- Dynamic DOM support — MutationObserver detects elements added after init
- Accessible by default — respects
prefers-reduced-motionautomatically - Tiny API surface — one function to call, one attribute to learn
Install
npm install motion-tokens gsap@"~3.13.0"GSAP
>=3.13.0 <3.14.0is a required peer dependency. All GSAP plugins (including SplitText) became free in 3.13. Version 3.14+ introduces breaking SplitText changes and is not supported.
Quick Start
import { initAnimations } from 'motion-tokens';
initAnimations();
// Done. Every [data-animate] element now animates on scroll.Token Grammar
Tokens follow a simple pattern:
data-animate="[effect](-[direction])(-[speed])(-[ease])(-[mode])(-[trigger])"Only effect is required. Everything after it is optional and order-independent — fade-up-slow-bounce and fade-bounce-slow-up both resolve identically.
Compound Effects
Combine multiple effects with the pipe (|) operator:
<div data-animate="fade|blur-up">Fade + blur from below</div>
<div data-animate="fade|scale-up-spring">Fade + scale with spring ease</div>When two effects write to the same CSS property, the last one wins. If a compound includes a text/stagger effect, non-text effects are dropped.
Token Reference
Effects
| Token | What it does |
|---------|------|
| fade | Opacity fade with optional directional offset |
| blur | Opacity fade + gaussian blur |
| scale | Scale up from smaller + fade |
| zoom | Scale up from center, no directional offset |
| pop | Scale up with a punchy back.out ease |
| slide | Slide in from off-screen |
| flip | 3D rotation reveal |
| clip | Clip-path reveal (direction sets the origin) |
| stagger | Animate child elements sequentially |
| words | Split text → reveal word by word |
| lines | Split text → reveal line by line |
| chars | Split text → reveal character by character |
| counter | Animate a number from start → end |
| draw | SVG stroke draw animation |
| float | Gentle floating idle loop (not scroll-triggered) |
| twist | 3D character reveal on scroll; use twist-hover for an interactive hover loop |
Directions
| Token | Meaning |
|-------|---------|
| up | From below |
| down | From above |
| left | From the left |
| right | From the right |
Most effects support directions. clip uses direction to set the clip origin. float, counter, draw, and twist ignore direction.
Speeds
| Token | Duration multiplier |
|-------|------|
| xfast | 0.6× |
| fast | 0.75× |
| (default) | 1.0× |
| slow | 1.5× |
| xslow | 2.0× |
Easing
| Token | Resolves to |
|-------|------------|
| bounce | back.out(1.7) |
| spring | elastic.out(1, 0.5) |
| elastic | elastic.out(1.2, 0.3) |
| expo | expo.out |
| power | power3.out |
Playback Modes
| Token | Behavior |
|-------|----------|
| scrub | Locks animation to scroll position |
| once | Plays once, then destroys the ScrollTrigger |
| norepeat | Plays on enter, does not reverse on scroll-up |
Triggers
| Token | Behavior |
|-------|----------|
| (default) | Scroll-triggered via ScrollTrigger |
| hover | Plays on mouseenter, reverses on mouseleave |
| click | Toggles on each click |
| focus | Plays on focus, reverses on blur |
Triggers can also be set separately via the data-trigger attribute.
Practical Examples
fade-up → Fade in from below
fade-left-fast → Fade in from left, 0.75× speed
blur-down-slow → Blur in from above, 1.5× speed
scale-up-spring → Scale + fade with elastic spring
flip-right-fast → 3D flip from right, fast
words-up-bounce → Word-by-word with bounce
fade-up-once → Fade up, plays once only
clip-left → Clip-path reveal from left
clip-down-slow → Clip reveal from top, slowly
counter → Animate number on scroll
draw → SVG stroke draw on scroll
float → Gentle floating idle loop
twist → 3D character reveal on scroll
twist-hover → 3D character hover loop
fade-up-hover → Fade in on hover
pop-click → Pop toggle on click
scale-up-scrub → Scale animation locked to scroll
fade|blur-up → Compound: fade + blur from below
fade|scale-up-expo → Compound: fade + scale with expo easeFine-Tuning with Data Attributes
Override any animation's defaults directly in HTML:
| Attribute | Type | Description |
|-----------|------|-------------|
| data-delay | number | Delay before animation starts (seconds) |
| data-duration | number | Override duration (seconds) |
| data-no-repeat | boolean | Play once, don't reverse on scroll-up |
| data-scroll-range | string | Custom scroll range as viewport % ("start-end") |
| data-stagger | number | Custom gap between stagger children (seconds) |
| data-stagger-from | string | Origin for text reveals: start, end, center, edges, random |
| data-trigger | string | Override trigger: scroll, hover, click, focus |
| data-animate-out | string | Separate animation token for leaving the viewport |
| data-sequence | string | Group name for sequenced animations |
| data-sequence-order | number | Order within a sequence group |
| data-counter-start | number | Starting value for counter |
| data-counter-decimals | number | Decimal places for counter |
Usage Examples
<!-- Delayed, plays once -->
<div data-animate="fade-up" data-delay="0.3" data-no-repeat>
Delayed entrance, no replay
</div>
<!-- Custom text timing -->
<p data-animate="words-up" data-duration="2" data-stagger="0.1">
Slower word reveal with wider gaps
</p>
<!-- Separate enter/exit animations -->
<div data-animate="fade-up" data-animate-out="fade-down">
Fades up on enter, fades down on leave
</div>
<!-- Sequenced group — elements animate in order -->
<h2 data-animate="fade-up" data-sequence="hero" data-sequence-order="1">Title</h2>
<p data-animate="fade-up" data-sequence="hero" data-sequence-order="2" data-delay="0.2">Subtitle</p>
<button data-animate="scale" data-sequence="hero" data-sequence-order="3" data-delay="0.4">CTA</button>
<!-- Counter -->
<span data-animate="counter" data-counter-start="0" data-counter-decimals="0">1500</span>
<!-- SVG draw -->
<path data-animate="draw" d="M10 80 C 40 10, 65 10, 95 80" />
<!-- Float (no scroll trigger) -->
<div data-animate="float">Gently bobs up and down</div>
<!-- Twist variants -->
<h2 data-animate="twist">3D character reveal on scroll</h2>
<h2 data-animate="twist-hover">3D character reveal on hover</h2>
<!-- Stagger-from control -->
<h1 data-animate="chars-up" data-stagger-from="random">Random character reveal</h1>
<p data-animate="words-up" data-stagger-from="center">Words from center outward</p>
<p data-animate="lines" data-stagger-from="end">Lines reveal bottom to top</p>API
initAnimations(options?)
Initialize the system. Returns an AnimationSystem handle.
import { initAnimations } from 'motion-tokens';
const system = initAnimations({
defaultDuration: 0.8,
movement: 40,
dev: true,
// CDN / non-bundler usage: inject GSAP manually
gsap: window.gsap,
scrollTrigger: window.ScrollTrigger,
// Register custom semantic names
fallbackRegistry: {
'hero-entrance': { effectImpl: 'fade', x: 0, y: 50, blur: 0 },
},
});AnimationSystem
| Member | Description |
|--------|-------------|
| destroy() | Kill all animations, remove MutationObserver, clean up |
| refresh() | Re-scan the DOM for new data-animate elements |
| engine | Access the underlying AnimationEngine |
| reducedMotion | true if the user prefers reduced motion |
Configuration
All values below can be passed to initAnimations():
| Option | Default | Description |
|--------|---------|-------------|
| preset | — | Apply a named style preset as a baseline: "minimal", "editorial", "expressive", "playful" |
| defaultDuration | 0.45 | Base duration (seconds) |
| slideDuration | 0.7 | Duration for slide |
| textDuration | 0.6 | Duration for text effects |
| staggerDuration | 0.4 | Duration per stagger child |
| movement | 24 | Directional offset (px) |
| slideDistance | 50 | slide travel distance (px) |
| blur | 3 | Blur amount (px) |
| scaleStart | 0.92 | Starting scale for scale / zoom |
| triggerPosition | "top 85%" | When ScrollTrigger fires |
| scrubSmoothing | 1.2 | Scrub smoothing factor |
| defaultStagger | 0.08 | Gap between stagger children (s) |
| textStagger | 0.05 | Gap between text segments (s) |
| clipDuration | 0.7 | Duration for clip |
| counterDuration | 1.2 | Duration for counter |
| drawDuration | 1.0 | Duration for SVG draw |
| floatDistance | 8 | Float bob distance (px) |
| floatDuration | 2.5 | Float cycle duration (s) |
| defaultEase | "power3.out" | Default easing |
| respectReducedMotion | true | Respect the OS reduced-motion setting |
| dev | false | Enable console warnings for invalid tokens |
Presets
Apply a named motion vocabulary in one line. Explicit options always override the preset.
initAnimations({ preset: 'editorial' });
initAnimations({ preset: 'expressive', movement: 60 }); // override one value| Preset | Vibe | Reference |
|--------|------|-----------|
| "minimal" | Restrained, fast — short durations, subtle movement | Linear, Apple |
| "editorial" | Polished, purposeful — mirrors the updated base defaults | Stripe, Vercel |
| "expressive" | Dramatic, bold — large distances, expo easing | Awwwards / agency |
| "playful" | Springy, energetic — back easing, snappy timing | Consumer apps |
Dynamic DOM
motion-tokens sets up a MutationObserver at init. Any element with data-animate added to the DOM later — via a framework router, lazy loading, or manual insertion — is automatically picked up. No manual refresh() call needed.
Accessibility
When the user's OS has prefers-reduced-motion: reduce enabled, all animations collapse to a simple 0.3 s opacity fade — no movement, blur, or scale. The float effect is skipped entirely.
To opt out of this behavior:
initAnimations({ respectReducedMotion: false });CDN Usage
For projects without a bundler:
<script type="importmap">
{
"imports": {
"gsap": "https://cdn.jsdelivr.net/npm/[email protected]/+esm",
"gsap/ScrollTrigger": "https://cdn.jsdelivr.net/npm/[email protected]/ScrollTrigger.js/+esm",
"gsap/SplitText": "https://cdn.jsdelivr.net/npm/[email protected]/SplitText.js/+esm"
}
}
</script>
<script type="module">
import { initAnimations } from './node_modules/motion-tokens/dist/index.js';
initAnimations({ dev: true });
</script>Peer Dependencies
| Package | Version | Notes |
|---------|---------|-------|
| gsap | >=3.13.0 <3.14.0 | All GSAP plugins (including SplitText) are free since 3.13. 3.14+ has breaking SplitText changes. |
License
MIT
