@devepoling-nl/data-motion
v0.0.1
Published
Utility-class syntax for scroll-triggered, in-view, and instant animations via a single data-motion attribute. Thin wrapper around Motion One.
Readme
Data Motion: animate anything with one HTML attribute
data-motion is a lightweight JavaScript animation library that drives scroll, in-view, and instant web animations
from a single HTML data-motion attribute. No animation config file, no per-element JS. It's a thin, declarative
wrapper around Motion One, the fast, small ( < 5kb) animation library for the web.
<div data-motion="fade-up">Fades and slides up as you scroll to it</div>Write the animation as a Tailwind-style utility string directly on the HTML element that has it. Framework-agnostic, works in plain HTML, Blade, JSX, Vue templates, or any other server-rendered or client-rendered markup.
Why data-motion
- One data attribute, zero animation JS to write.
data-motion="fade-up"is the whole implementation. - Utility-class syntax. Familiar if you already write Tailwind CSS utility classes; compose
opacity,scale,x/y,duration,delay,stagger, and easing tokens directly in the attribute. - Scroll-triggered, in-view (IntersectionObserver), and instant animation modes in one API.
- Presets with per-element overrides. Define a
fade-uppreset once, overrideduration/delay/easeper element without writing new CSS or JS. - Built on Motion One. Hardware-accelerated Web Animations API under the hood, not a custom animation engine.
- No build step, no bundler config. Plain ESM, tree-shakeable (
sideEffects: false), TypeScript types included.
Install
npm install @devepoling-nl/data-motion motionmotion is a peer dependency. Install it alongside.
Quick start
import {initMotion} from "@devepoling-nl/data-motion";
initMotion();Call initMotion() once your DOM is ready. Nothing runs on import. This package has no import-time side effects, so
it's safe to use in SSR, tests, or bundlers that tree-shake on sideEffects: false.
<script type="module">
import {initMotion} from "@devepoling-nl/data-motion";
initMotion();
</script>
<div data-motion="fade-up">Hello</div>Dynamic content
initMotion() skips elements it has already initialised, so call it again after appending new DOM (Livewire morphs,
htmx swaps, client-side router changes):
htmx.on("htmx:afterSwap", () => initMotion());Pass a root to scope the scan instead of scanning the whole document:
initMotion(document.querySelector("#new-section"));Cleanup
Both initMotion() and applyMotion() return a disposer. Call it to stop running animations / scroll or in-view
watchers. Useful before removing elements or on SPA navigation:
const stop = initMotion();
// later
stop();Presets
import {presets} from "@devepoling-nl/data-motion";
presets["fade-up"] = "scroll-trigger opacity-100 y-[50,0] duration-800 ease-out";
presets["fade-up-children"] = "scroll-trigger opacity-100 y-[50,0] duration-750 stagger-300 ease-out children";Use a preset by name, and append extra tokens as overrides. They're appended after the preset's own tokens, so they win:
<div data-motion="fade-up"></div>
<div data-motion="fade-up duration-1200 delay-300"></div>
<div data-motion="fade-up trigger-point-30 ease-in-out"></div>
<ul data-motion="fade-up-children stagger-200"></ul>Add your own presets the same way. presets is a plain mutable object, keyed by whatever name you want to use after
data-motion="…".
Full syntax reference
A data-motion value is a space-separated list of tokens. Skip the preset name entirely to write the full utility
string:
<div data-motion="scroll-trigger opacity-[0,100] y-[50,0] duration-800 ease-out"></div>Mode (pick one, default animate)
| Token | Behaviour |
|------------------|--------------------------------------------------------------------------------|
| scroll-trigger | Animates once, when scroll progress through the element passes trigger-point |
| in-view | Animates once, when the element enters the viewport (IntersectionObserver) |
| animate | Animates immediately (this is the default if no mode token is given) |
Target
| Token | Behaviour |
|------------|--------------------------------------------------------------------------|
| children | Animates el.children instead of el itself. Combine with stagger-N |
Timing
| Token | Behaviour |
|-------------------|---------------------------------------------------------------------------------------------|
| duration-N | Animation duration, N in milliseconds |
| delay-N | Start delay, N in milliseconds |
| stagger-N | Per-child delay (with children), N in milliseconds |
| trigger-point-N | For scroll-trigger/in-view: fire at N% scroll progress / visible amount (default 5) |
Easing
linear ease ease-in ease-out ease-in-out circ-in circ-out circ-in-out back-in back-out back-in-out
Keyframe properties
opacity, scale, y, x. Each takes either a single value (animate to it, from the current value) or a
bracketed pair (animate between the two):
y-50 → animate to y: 50
y-[50,0] → animate from y: 50 to y: 0
opacity-80 → animate to opacity: 0.8 (values >1 treated as a percentage)
opacity-[0,100] → animate from 0 to 1
scale-85 → animate to scale: 0.85
-y-50 → negative: animate to y: -50
-y-[50,0] → animate from y: -50 to y: 0A leading - negates the whole value (or both values in a bracket pair). At least one keyframe property is required. A
data-motion value with only mode/timing tokens and no opacity/scale/y/x does nothing (and logs a warning).
Preventing a flash of visible content
For scroll-trigger / in-view modes, the animated element sits fully visible in the DOM until its trigger fires,
so it can flash into view before snapping to its pre-animation state and fading in. Import the optional base
stylesheet to pre-hide the two default presets:
import "@devepoling-nl/data-motion/base.css";or in CSS directly:
@import "@devepoling-nl/data-motion/base.css";It only covers exact matches on fade-up and fade-up-children, the presets shipped by default. A custom preset
or a raw utility string (data-motion="in-view opacity-[0,100] scale-[85,100]") needs its own matching rule, e.g.:
[data-motion="my-preset"] {
opacity: 0;
transform: scale(0.85);
}Use the preset's first keyframe value as the CSS starting state. If the stylesheet isn't loaded, or the JS never runs, elements simply degrade to visible/normal position. They're never permanently hidden by this alone.
Advanced: single-element API
applyMotion(el, utilityStr) applies a raw (non-preset) utility string to one element directly. Useful for animating
something outside the declarative data-motion scan, e.g. an element created purely in JS:
import {applyMotion} from "@devepoling-nl/data-motion";
const stop = applyMotion(myElement, "animate opacity-[0,100] duration-400 ease-out");Debugging
Unrecognised tokens and unparsable values log a console.warn naming the exact token. Check the console first if an
animation isn't firing.
AI / Claude Code
This package ships SKILL.md. Copy or symlink it into your project's .claude/skills/data-motion/ so
Claude Code has the full data-motion syntax reference on hand when you ask it to add or edit animations.
mkdir -p .claude/skills/data-motion && cp node_modules/@devepoling-nl/data-motion/SKILL.md .claude/skills/data-motion/SKILL.mdSymlink instead, to stay in sync with the installed package version on update:
mkdir -p .claude/skills/data-motion && ln -sf ../../../node_modules/@devepoling-nl/data-motion/SKILL.md .claude/skills/data-motion/SKILL.mdRelated
- Motion One. The JavaScript animation engine
data-motionwraps, built on the native Web Animations API. Use it directly for gestures, drag, and multi-step timelines outside whatdata-motion's attribute syntax covers. - Motion One documentation.
duration,delay,easing, andstaggeroptions indata-motion's timing tokens map 1:1 to Motion One's own animation options.
Changelog
See CHANGELOG.md for recent changes.
Contributing
Issues and pull requests are welcome. Open one against the GitHub repo.
Keep the core parser dependency-free (only motion as a peer dependency) and add a usage example to the README for any
new token.
Security
If you find a security vulnerability, email [email protected] instead of opening a public issue.
Credits
- Devepoling
- Motion One. The animation engine this package wraps
- All contributors
Alternatives
Other libraries that drive animation from HTML attributes or utility syntax, if data-motion isn't the right fit:
- glaze. Utility-first animation framework built on GSAP, composes animations from data-attribute tokens with responsive variants.
- AOS (Animate On Scroll).
data-aos="fade-up"style scroll animations, CSS-transition based rather than a JS animation engine. - ScrollReveal. JS scroll-animation library configured via a JS options object rather than markup.
- GSAP + ScrollTrigger. Full-featured, code-driven animation and scroll-trigger engine; reach for this when you need timelines, pinning, or scrubbing beyond what a single attribute can express.
Who we are
data-motion is built and maintained by Devepoling, a software development agency in the
Netherlands. We build tailored applications, websites, and digital products with a privacy-first approach.
License
MIT
