Motion One
Lightweight Web Animations API wrapper — animate, timeline, spring physics, scroll-linked animations, stagger, and minimal bundle size for modern browsers.
Motion One is a thin, performant animation library built on top of the browser's native Web Animations API (WAAPI). Instead of reimplementing an animation engine in JavaScript, it delegates interpolation to the browser, resulting in a tiny bundle (~3.8KB gzipped) and hardware-accelerated animations by default.
## Key Points
- **Snappy UI feedback:** `spring({ stiffness: 400, damping: 30 })`
- **Bouncy entrance:** `spring({ stiffness: 200, damping: 15 })`
- **Smooth settle:** `spring({ stiffness: 100, damping: 20 })`
- **Rely on WAAPI for transforms and opacity.** These properties animate on the compositor thread, meaning zero main-thread cost. Motion One sends them to WAAPI by default.
- **Use `spring()` for interactive feedback.** Springs respond naturally to interruption — if a user hovers off mid-animation, the spring smoothly reverses without jarring cuts.
- **Stop animations on cleanup.** Always call `controls.stop()` in the useEffect return function to prevent animations from running after component unmount.
- **Prefer element refs over selectors in React.** Using `".class"` selectors works but can match elements outside your component. Use refs for isolation.
- **Use timeline `at` for orchestration.** The `at` parameter (`"<"`, `"-0.3"`, `"+0.1"`) gives precise control over overlap without manual delay calculations.
- **Set initial styles in CSS or inline styles.** If animating from `opacity: 0`, set `style={{ opacity: 0 }}` on the element to prevent flash-of-unstyled-content before JS executes.
- **Leverage the tiny bundle size.** At ~3.8KB, Motion One is ideal for landing pages and marketing sites where bundle size directly impacts Core Web Vitals.
- **Animating layout properties (width, height, top, left).** These trigger layout recalculation on every frame. Use `transform` properties (`x`, `y`, `scale`) which composite on the GPU.
- **Forgetting to stop animations.** Leaked animations continue running their requestAnimationFrame callbacks, consuming CPU even when the component is gone.
## Quick Example
```bash
npm install motion
```
```bash
npm install @motionone/dom
```skilldb get animation-services-skills/Motion OneFull skill: 331 linesMotion One
Core Philosophy
Motion One is a thin, performant animation library built on top of the browser's native Web Animations API (WAAPI). Instead of reimplementing an animation engine in JavaScript, it delegates interpolation to the browser, resulting in a tiny bundle (~3.8KB gzipped) and hardware-accelerated animations by default.
Key mental model: use the browser's animation engine, enhance it with a better API. Motion One adds springs, timelines, stagger, and scroll-linked animations on top of what WAAPI already provides natively. Animations run on the compositor thread whenever possible, keeping the main thread free.
Setup
npm install motion
Note: Motion One has been merged into the broader motion package. For standalone usage:
npm install @motionone/dom
import { animate, timeline, stagger, scroll, spring } from "motion";
Minimal Example
import { useRef, useEffect } from "react";
import { animate } from "motion";
export function FadeIn({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
const controls = animate(
ref.current,
{ opacity: [0, 1], y: [20, 0] },
{ duration: 0.5, easing: "ease-out" }
);
return () => controls.stop();
}, []);
return <div ref={ref}>{children}</div>;
}
Key Techniques
The animate Function
The core API accepts an element (or selector), keyframes, and options.
import { animate } from "motion";
// Single element
animate("#box", { x: 200, rotate: 45 }, { duration: 0.6 });
// Multiple properties with individual settings
animate(".card", {
opacity: [0, 1],
y: [30, 0],
scale: [0.95, 1],
}, {
duration: 0.5,
easing: "ease-out",
delay: 0.1,
});
// Animate any value (not just DOM)
animate(
(progress: number) => {
console.log(`Progress: ${Math.round(progress * 100)}%`);
},
{ duration: 1 }
);
Spring Physics
Motion One supports spring easing that generates natural, physics-based motion.
import { animate, spring } from "motion";
animate(".modal", {
opacity: [0, 1],
scale: [0.9, 1],
}, {
easing: spring({ stiffness: 300, damping: 20, mass: 1 }),
});
Spring configuration guide:
- Snappy UI feedback:
spring({ stiffness: 400, damping: 30 }) - Bouncy entrance:
spring({ stiffness: 200, damping: 15 }) - Smooth settle:
spring({ stiffness: 100, damping: 20 })
Stagger
import { animate, stagger } from "motion";
// Stagger a list of elements
animate(
".list-item",
{ opacity: [0, 1], x: [-20, 0] },
{ delay: stagger(0.08), duration: 0.4, easing: "ease-out" }
);
// Stagger from center
animate(
".grid-item",
{ scale: [0, 1] },
{ delay: stagger(0.05, { from: "center" }), easing: spring() }
);
// Stagger with easing (items accelerate into the stagger)
animate(
".card",
{ opacity: [0, 1], y: [40, 0] },
{ delay: stagger(0.06, { easing: "ease-in" }), duration: 0.5 }
);
Timeline
Sequence multiple animations with precise timing control.
import { timeline } from "motion";
const sequence: Parameters<typeof timeline>[0] = [
[".hero-title", { opacity: [0, 1], y: [40, 0] }, { duration: 0.6 }],
[".hero-subtitle", { opacity: [0, 1], y: [30, 0] }, { duration: 0.5, at: "-0.3" }],
[".hero-cta", { opacity: [0, 1], scale: [0.9, 1] }, { duration: 0.4, at: "-0.2" }],
[".hero-image", { opacity: [0, 1], x: [60, 0] }, { duration: 0.8, at: "<" }],
];
const controls = timeline(sequence, {
defaultOptions: { easing: "ease-out" },
});
React Integration with Timeline
import { useRef, useEffect } from "react";
import { timeline } from "motion";
export function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const controls = timeline([
[containerRef.current.querySelector(".title")!, { opacity: [0, 1], y: [30, 0] }, { duration: 0.6 }],
[containerRef.current.querySelector(".body")!, { opacity: [0, 1], y: [20, 0] }, { duration: 0.5, at: "-0.3" }],
[containerRef.current.querySelector(".cta")!, { opacity: [0, 1] }, { duration: 0.4, at: "-0.2" }],
]);
return () => controls.stop();
}, []);
return (
<section ref={containerRef}>
<h1 className="title" style={{ opacity: 0 }}>Welcome</h1>
<p className="body" style={{ opacity: 0 }}>Build fast, animate faster.</p>
<button className="cta" style={{ opacity: 0 }}>Get Started</button>
</section>
);
}
Scroll-Linked Animations
import { scroll, animate } from "motion";
// Progress bar tied to page scroll
scroll(animate(".progress-bar", { scaleX: [0, 1] }));
// Animate when element enters viewport
scroll(
animate(".feature-card", { opacity: [0, 1], y: [50, 0] }),
{
target: document.querySelector(".feature-card")!,
offset: ["start end", "end end"],
}
);
React Scroll Pattern
import { useRef, useEffect } from "react";
import { scroll, animate } from "motion";
export function ScrollReveal({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
const cleanup = scroll(
animate(ref.current, { opacity: [0, 1], y: [40, 0] }, { easing: "ease-out" }),
{
target: ref.current,
offset: ["start 90%", "start 50%"],
}
);
return () => cleanup();
}, []);
return <div ref={ref} style={{ opacity: 0 }}>{children}</div>;
}
Controlling Playback
The animate and timeline functions return an AnimationControls object.
import { useRef, useState, useEffect } from "react";
import { animate, type AnimationControls } from "motion";
export function PlaybackDemo() {
const ref = useRef<HTMLDivElement>(null);
const controlsRef = useRef<AnimationControls | null>(null);
useEffect(() => {
if (!ref.current) return;
controlsRef.current = animate(
ref.current,
{ x: [0, 300] },
{ duration: 2, easing: "ease-in-out", direction: "alternate", repeat: Infinity }
);
return () => controlsRef.current?.stop();
}, []);
return (
<div>
<div ref={ref} className="w-12 h-12 bg-blue-500 rounded" />
<div className="flex gap-2 mt-4">
<button onClick={() => controlsRef.current?.play()}>Play</button>
<button onClick={() => controlsRef.current?.pause()}>Pause</button>
<button onClick={() => controlsRef.current?.reverse()}>Reverse</button>
<button onClick={() => {
if (controlsRef.current) controlsRef.current.currentTime = 0;
}}>Reset</button>
</div>
</div>
);
}
Custom Hooks
import { useRef, useEffect, useCallback } from "react";
import { animate, type AnimationOptions } from "motion";
export function useAnimate<T extends HTMLElement>() {
const ref = useRef<T>(null);
const controlsRef = useRef<ReturnType<typeof animate> | null>(null);
const play = useCallback(
(keyframes: Record<string, any>, options?: AnimationOptions) => {
if (!ref.current) return;
controlsRef.current?.stop();
controlsRef.current = animate(ref.current, keyframes, options);
return controlsRef.current;
},
[]
);
useEffect(() => {
return () => controlsRef.current?.stop();
}, []);
return { ref, play };
}
// Usage
export function InteractiveCard() {
const { ref, play } = useAnimate<HTMLDivElement>();
return (
<div
ref={ref}
onMouseEnter={() => play({ scale: 1.05, y: -4 }, { duration: 0.2 })}
onMouseLeave={() => play({ scale: 1, y: 0 }, { duration: 0.2 })}
className="p-6 bg-white rounded-xl shadow"
>
Hover me
</div>
);
}
Best Practices
- Rely on WAAPI for transforms and opacity. These properties animate on the compositor thread, meaning zero main-thread cost. Motion One sends them to WAAPI by default.
- Use
spring()for interactive feedback. Springs respond naturally to interruption — if a user hovers off mid-animation, the spring smoothly reverses without jarring cuts. - Stop animations on cleanup. Always call
controls.stop()in the useEffect return function to prevent animations from running after component unmount. - Prefer element refs over selectors in React. Using
".class"selectors works but can match elements outside your component. Use refs for isolation. - Use timeline
atfor orchestration. Theatparameter ("<","-0.3","+0.1") gives precise control over overlap without manual delay calculations. - Set initial styles in CSS or inline styles. If animating from
opacity: 0, setstyle={{ opacity: 0 }}on the element to prevent flash-of-unstyled-content before JS executes. - Leverage the tiny bundle size. At ~3.8KB, Motion One is ideal for landing pages and marketing sites where bundle size directly impacts Core Web Vitals.
Anti-Patterns
- Animating layout properties (width, height, top, left). These trigger layout recalculation on every frame. Use
transformproperties (x,y,scale) which composite on the GPU. - Forgetting to stop animations. Leaked animations continue running their requestAnimationFrame callbacks, consuming CPU even when the component is gone.
- Using Motion One for complex choreography that needs scrubbing. For multi-minute, scrubbable timelines with pinning, GSAP's ScrollTrigger is more battle-tested. Motion One excels at discrete, composable micro-animations.
- Over-using selectors instead of direct element references.
animate(".card", ...)queries the entire document. In component architectures, scope to refs or usecontainer.querySelectorAll. - Chaining
animatecalls without timeline. Callinganimatetwice on the same element overwrites the first animation. Usetimelineto sequence them or use thefinishedpromise to chain. - Ignoring the
finishedpromise. Every animation returns afinishedpromise. Use it to trigger follow-up logic instead of guessing durations withsetTimeout.
Install this skill directly: skilldb add animation-services-skills
Related Skills
Anime.js
Lightweight JavaScript animation engine — DOM, CSS, SVG, and object property animations with timeline sequencing, staggering, and spring-based easing.
Auto Animate
Zero-config animation library by FormKit — automatic transitions for DOM additions, removals, and reordering with a single function call or directive.
Framer Motion (Motion)
Production-grade animation library for React — animate, variants, AnimatePresence, layout animations, gestures, scroll-triggered effects, useMotionValue, and stagger orchestration.
GSAP
GreenSock Animation Platform — timeline orchestration, ScrollTrigger, tweens, stagger, React integration with useGSAP, SplitText, morphSVG, and from/to/fromTo patterns.
Lottie
Render After Effects animations on the web with lottie-react and lottie-web — player controls, interactivity, lazy loading, and the light player for optimized bundles.
Popmotion
Functional animation library — spring physics, decay, keyframe interpolation, and composable animation primitives that power Framer Motion's internals.