Motion kits
A few that match beat a hundred that don’t.
Finding one animation you like is easy. Finding 20 that feel like they belong together is the hard part, and it’s what you need once four of them share a screen. Each kit lists the timing rules it was built on, and every animation in it follows them.
SaaS dashboard
5 animationsData arriving, without making anyone wait to read it.
Dashboard motion happens while someone is trying to read a number. Short travel, stiff settle, no overshoot: the animation's whole job is to say 'this is new' and then get out of the way. A bouncy stat counter is a stat counter you have to wait for.
Stat row
on loadThree numbers land left to right, tight enough to read as one row.
import { motion, type Variants } from "motion/react";
// 3 siblings running one animation 60ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.06,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, y: 8 },
show: {
opacity: [0, 1],
y: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 480, damping: 34, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
},
},
} satisfies Variants;
export function StatRow() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex gap-3">
<motion.div
className="flex-1 rounded-lg border border-border bg-background p-3"
variants={item}
>
<span className="block text-[10px] uppercase tracking-[0.12em] text-muted-foreground">Revenue</span>
<span className="mt-1 block text-[20px] font-semibold text-foreground">$48.2k</span>
</motion.div><motion.div
className="flex-1 rounded-lg border border-border bg-background p-3"
variants={item}
>
<span className="block text-[10px] uppercase tracking-[0.12em] text-muted-foreground">Active</span>
<span className="mt-1 block text-[20px] font-semibold text-foreground">1,284</span>
</motion.div><motion.div
className="flex-1 rounded-lg border border-border bg-background p-3"
variants={item}
>
<span className="block text-[10px] uppercase tracking-[0.12em] text-muted-foreground">Churn</span>
<span className="mt-1 block text-[20px] font-semibold text-foreground">1.8%</span>
</motion.div>
</div></motion.div>
);
}
Table rows
on data loadRows arrive in order. Nothing overshoots, because a moving row is unreadable.
import { motion, type Variants } from "motion/react";
// 4 siblings running one animation 60ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.06,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, y: 4 },
show: {
opacity: [0, 1],
y: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 480, damping: 34, mass: 1 },
},
},
} satisfies Variants;
export function TableRows() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex flex-col gap-1.5">
<motion.div
className="flex items-center justify-between rounded-md border border-border bg-background px-3 py-2"
variants={item}
>
<span className="text-[12px] text-foreground">Acme Corp</span>
<span className="text-[11px] text-muted-foreground">62 users</span>
</motion.div><motion.div
className="flex items-center justify-between rounded-md border border-border bg-background px-3 py-2"
variants={item}
>
<span className="text-[12px] text-foreground">Globex</span>
<span className="text-[11px] text-muted-foreground">93 users</span>
</motion.div><motion.div
className="flex items-center justify-between rounded-md border border-border bg-background px-3 py-2"
variants={item}
>
<span className="text-[12px] text-foreground">Initech</span>
<span className="text-[11px] text-muted-foreground">124 users</span>
</motion.div><motion.div
className="flex items-center justify-between rounded-md border border-border bg-background px-3 py-2"
variants={item}
>
<span className="text-[12px] text-foreground">Umbrella</span>
<span className="text-[11px] text-muted-foreground">155 users</span>
</motion.div>
</div></motion.div>
);
}
Chart bars
on loadBars grow from the axis rather than fading — height IS the data.
import { motion, type Variants } from "motion/react";
// 7 siblings running one animation 40ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.04,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, scale: 0.6 },
show: {
opacity: [0, 1],
scale: 1,
transition: {
opacity: { duration: 0.14, times: [0, 1], ease: "easeOut" },
scale: { type: "spring", stiffness: 480, damping: 34, mass: 1 },
},
},
} satisfies Variants;
export function ChartBars() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex h-24 items-end gap-2">
<motion.div
className="flex-1 rounded-t bg-primary/70"
style={{ height: "40%" }}
variants={item}
></motion.div><motion.div
className="flex-1 rounded-t bg-primary/70"
style={{ height: "65%" }}
variants={item}
></motion.div><motion.div
className="flex-1 rounded-t bg-primary/70"
style={{ height: "45%" }}
variants={item}
></motion.div><motion.div
className="flex-1 rounded-t bg-primary/70"
style={{ height: "80%" }}
variants={item}
></motion.div><motion.div
className="flex-1 rounded-t bg-primary/70"
style={{ height: "55%" }}
variants={item}
></motion.div><motion.div
className="flex-1 rounded-t bg-primary/70"
style={{ height: "95%" }}
variants={item}
></motion.div><motion.div
className="flex-1 rounded-t bg-primary/70"
style={{ height: "70%" }}
variants={item}
></motion.div>
</div></motion.div>
);
}
Empty state
when a list is emptyThe slowest thing in this kit, deliberately — nothing is waiting on it.
import { motion } from "motion/react";
export function EmptyState() {
return (
<div className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"><div className="flex flex-col items-center gap-2 py-6 text-center">
<motion.div
className="size-10 rounded-full border border-border bg-background"
initial={{ opacity: 0, y: 4, scale: 0.8 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 480, damping: 34, mass: 1 },
scale: { type: "spring", stiffness: 480, damping: 34, mass: 1 },
}}
></motion.div>
<motion.span
className="text-[13px] text-foreground"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.08 },
y: { type: "spring", stiffness: 480, damping: 34, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.08 },
}}
>No customers yet</motion.span>
<motion.span
className="text-[11px] text-muted-foreground"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.14 },
y: { type: "spring", stiffness: 480, damping: 34, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.14 },
}}
>They will appear here as they sign up.</motion.span>
</div></div>
);
}
Marketing site
5 animationsMotion with room to breathe, for people deciding whether to care.
Marketing motion happens while someone is deciding whether to care, so it can afford room. Longer travel and a softer spring read as confidence; the same distance at dashboard speed reads as a page that is in a hurry to sell you something.
Hero entrance
on loadBadge, headline, body, buttons — in reading order, which is the only order.
Ship motion you can read.
Design it by touching it, then take the code with you.
import { motion, type Variants } from "motion/react";
// 4 siblings running one animation 160ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.16,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, y: 28 },
show: {
opacity: [0, 1],
y: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
},
},
} satisfies Variants;
export function HeroEntrance() {
return (
<motion.div
className="w-full max-w-[480px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex flex-col gap-4">
<motion.span
className="w-fit rounded-full border border-primary/40 bg-primary/10 px-3 py-1 text-[11px] text-primary"
variants={item}
>New</motion.span>
<motion.h1
className="text-[30px] font-semibold leading-tight text-foreground"
variants={item}
>Ship motion you can read.</motion.h1>
<motion.p
className="text-[13px] leading-relaxed text-muted-foreground"
variants={item}
>Design it by touching it, then take the code with you.</motion.p>
<motion.div
className="flex gap-2"
variants={item}
>
<span className="rounded-lg bg-primary px-4 py-2 text-[12px] font-medium text-primary-foreground">Start free</span>
<span className="rounded-lg border border-border px-4 py-2 text-[12px] text-muted-foreground">See an export</span>
</motion.div>
</div></motion.div>
);
}
Feature grid
on scroll into viewA grid reveals along the reading diagonal, not row by row.
import { motion } from "motion/react";
export function FeatureGrid() {
return (
<div className="w-full max-w-[440px] rounded-xl border border-border bg-card p-5"><div className="grid grid-cols-2 gap-3">
<motion.div
className="rounded-lg border border-border bg-background p-4"
initial={{ opacity: 0, y: 28, scale: 0.96 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
scale: { type: "spring", stiffness: 120, damping: 20, mass: 1 },
}}
>
<div className="size-6 rounded-md bg-primary/20"></div>
<span className="mt-2 block text-[12px] text-foreground">Timeline</span>
</motion.div><motion.div
className="rounded-lg border border-border bg-background p-4"
initial={{ opacity: 0, y: 28, scale: 0.96 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.16 },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.16 },
scale: { type: "spring", stiffness: 120, damping: 20, mass: 1, delay: 0.16 },
}}
>
<div className="size-6 rounded-md bg-primary/20"></div>
<span className="mt-2 block text-[12px] text-foreground">Springs</span>
</motion.div><motion.div
className="rounded-lg border border-border bg-background p-4"
initial={{ opacity: 0, y: 28, scale: 0.96 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.16 },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.16 },
scale: { type: "spring", stiffness: 120, damping: 20, mass: 1, delay: 0.16 },
}}
>
<div className="size-6 rounded-md bg-primary/20"></div>
<span className="mt-2 block text-[12px] text-foreground">Export</span>
</motion.div><motion.div
className="rounded-lg border border-border bg-background p-4"
initial={{ opacity: 0, y: 28, scale: 0.96 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.32 },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.32 },
scale: { type: "spring", stiffness: 120, damping: 20, mass: 1, delay: 0.32 },
}}
>
<div className="size-6 rounded-md bg-primary/20"></div>
<span className="mt-2 block text-[12px] text-foreground">Parity</span>
</motion.div>
</div></div>
);
}
Logo strip
on scroll into viewFades only. Logos that slide look like they are falling off the page.
import { motion, type Variants } from "motion/react";
// 5 siblings running one animation 60ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.06,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0 },
show: {
opacity: [0, 1],
transition: {
opacity: { duration: 0.32, times: [0, 1], ease: "easeOut" },
},
},
} satisfies Variants;
export function LogoStrip() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex items-center justify-between gap-4">
<motion.div
className="h-6 w-16 rounded bg-muted-foreground/25"
variants={item}
></motion.div><motion.div
className="h-6 w-16 rounded bg-muted-foreground/25"
variants={item}
></motion.div><motion.div
className="h-6 w-16 rounded bg-muted-foreground/25"
variants={item}
></motion.div><motion.div
className="h-6 w-16 rounded bg-muted-foreground/25"
variants={item}
></motion.div><motion.div
className="h-6 w-16 rounded bg-muted-foreground/25"
variants={item}
></motion.div>
</div></motion.div>
);
}
Testimonial
on scroll into viewThe quote leads; the attribution follows, because that is the reading order.
"The export is the product. I read it before I trusted it."
import { motion, type Variants } from "motion/react";
// 2 siblings running one animation 160ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.16,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, y: 28 },
show: {
opacity: [0, 1],
y: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
},
},
} satisfies Variants;
export function Testimonial() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex flex-col gap-3">
<motion.p
className="text-[15px] leading-relaxed text-foreground"
variants={item}
>"The export is the product. I read it before I trusted it."</motion.p>
<motion.div
className="flex items-center gap-2"
variants={item}
>
<div className="size-7 rounded-full bg-muted-foreground/25"></div>
<span className="text-[11px] text-muted-foreground">Staff engineer, somewhere real</span>
</motion.div>
</div></motion.div>
);
}
Pricing card
on scroll into viewThe price arrives last. Everything before it is context for the number.
import { motion } from "motion/react";
export function PricingCard() {
return (
<div className="w-full max-w-[280px] rounded-xl border border-border bg-card p-5"><div className="flex flex-col gap-3">
<motion.span
className="text-[11px] uppercase tracking-[0.12em] text-muted-foreground"
initial={{ opacity: 0, y: 28 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
}}
>Pro</motion.span>
<motion.span
className="text-[28px] font-semibold text-foreground"
initial={{ opacity: 0, y: 28, scale: 0.92 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.32 },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.32 },
scale: { type: "spring", stiffness: 120, damping: 20, mass: 1, delay: 0.32 },
}}
>$12<span className="text-[13px] text-muted-foreground">/mo</span></motion.span>
<motion.span
className="text-[12px] text-muted-foreground"
initial={{ opacity: 0, y: 28 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.48 },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.48 },
}}
>Every dialect</motion.span><motion.span
className="text-[12px] text-muted-foreground"
initial={{ opacity: 0, y: 28 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.56 },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.56 },
}}
>Unlimited export</motion.span><motion.span
className="text-[12px] text-muted-foreground"
initial={{ opacity: 0, y: 28 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.64 },
y: { type: "spring", stiffness: 120, damping: 20, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.64 },
}}
>The whole library</motion.span>
</div></div>
);
}
E-commerce
5 animationsConfirmation you cannot miss, on pages where a dropped signal costs money.
Commerce motion confirms that an action landed, on a page where a dropped signal costs a sale. Immediate and unambiguous. The one thing it must never do is make someone wonder whether the button worked — which is exactly what a slow, soft confirmation does.
Product grid
on loadCards arrive together enough to read as a set, not as a queue.
import { motion, type Variants } from "motion/react";
// 3 siblings running one animation 100ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.1,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, y: 16, scale: 0.97 },
show: {
opacity: [0, 1],
y: 0,
scale: 1,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
scale: { type: "spring", stiffness: 260, damping: 24, mass: 1 },
},
},
} satisfies Variants;
export function ProductGrid() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="grid grid-cols-3 gap-3">
<motion.div
className="rounded-lg border border-border bg-background p-2"
variants={item}
>
<div className="aspect-square rounded bg-muted-foreground/15"></div>
<span className="mt-2 block text-[11px] text-foreground">Item 1</span>
<span className="block text-[11px] text-muted-foreground">$27</span>
</motion.div><motion.div
className="rounded-lg border border-border bg-background p-2"
variants={item}
>
<div className="aspect-square rounded bg-muted-foreground/15"></div>
<span className="mt-2 block text-[11px] text-foreground">Item 2</span>
<span className="block text-[11px] text-muted-foreground">$36</span>
</motion.div><motion.div
className="rounded-lg border border-border bg-background p-2"
variants={item}
>
<div className="aspect-square rounded bg-muted-foreground/15"></div>
<span className="mt-2 block text-[11px] text-foreground">Item 3</span>
<span className="block text-[11px] text-muted-foreground">$45</span>
</motion.div>
</div></motion.div>
);
}
Added to cart
on add to cartThe fastest confirmation in the set. Doubt here costs a sale.
import { motion } from "motion/react";
export function AddedToCart() {
return (
<div className="w-full max-w-[320px] rounded-xl border border-border bg-card p-5"><div className="flex items-center justify-between gap-3">
<motion.span
className="text-[13px] text-foreground"
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
}}
>Added to cart</motion.span>
<motion.span
className="rounded-lg bg-primary px-3 py-1.5 text-[12px] font-medium text-primary-foreground"
initial={{ opacity: 0, y: 16, scale: 0.94 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.1 },
y: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.1 },
scale: { type: "spring", stiffness: 260, damping: 24, mass: 1, delay: 0.1 },
}}
>Checkout</motion.span>
</div></div>
);
}
Cart drawer
on open cartThe panel arrives, then its contents — so the box is never empty on screen.
import { motion } from "motion/react";
export function CartDrawer() {
return (
<div className="w-full max-w-[300px] rounded-xl border border-border bg-card p-5"><div className="flex flex-col gap-2">
<motion.span
className="text-[12px] font-medium text-foreground"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
}}
>Your cart</motion.span>
<motion.div
className="flex items-center gap-2 rounded-md border border-border bg-background p-2"
initial={{ opacity: 0, x: 16 }}
animate={{ opacity: [0, 1], x: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.1 },
x: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.1 },
}}
>
<div className="size-8 rounded bg-muted-foreground/15"></div>
<span className="flex-1 text-[11px] text-foreground">Item 1</span>
<span className="text-[11px] text-muted-foreground">$27</span>
</motion.div><motion.div
className="flex items-center gap-2 rounded-md border border-border bg-background p-2"
initial={{ opacity: 0, x: 16 }}
animate={{ opacity: [0, 1], x: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.2 },
x: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.2 },
}}
>
<div className="size-8 rounded bg-muted-foreground/15"></div>
<span className="flex-1 text-[11px] text-foreground">Item 2</span>
<span className="text-[11px] text-muted-foreground">$36</span>
</motion.div>
</div></div>
);
}
Checkout steps
on step changeProgress fills forward only. A checkout bar that animates backwards reads as an error.
import { motion, type Variants } from "motion/react";
// 3 siblings running one animation 100ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.1,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, x: -8 },
show: {
opacity: [0, 1],
x: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
x: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
},
},
} satisfies Variants;
export function CheckoutSteps() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex items-center gap-2">
<motion.div
className="flex flex-1 flex-col gap-1"
variants={item}
>
<div className="h-1 rounded-full bg-primary"></div>
<span className="text-[10px] text-muted-foreground">Cart</span>
</motion.div><motion.div
className="flex flex-1 flex-col gap-1"
variants={item}
>
<div className="h-1 rounded-full bg-border"></div>
<span className="text-[10px] text-muted-foreground">Details</span>
</motion.div><motion.div
className="flex flex-1 flex-col gap-1"
variants={item}
>
<div className="h-1 rounded-full bg-border"></div>
<span className="text-[10px] text-muted-foreground">Pay</span>
</motion.div>
</div></motion.div>
);
}
Price change
on discount appliedThe new price arrives; the old one stays put and fades. Movement means change.
import { motion } from "motion/react";
export function PriceChange() {
return (
<div className="w-full max-w-[240px] rounded-xl border border-border bg-card p-5"><div className="flex items-baseline gap-2">
<motion.span
className="text-[22px] font-semibold text-foreground"
initial={{ opacity: 0, y: 8, scale: 0.9 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 260, damping: 24, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
scale: { type: "spring", stiffness: 260, damping: 24, mass: 1 },
}}
>$27.00</motion.span>
<motion.span
className="text-[13px] text-muted-foreground line-through"
initial={{ opacity: 1 }}
animate={{ opacity: [1, 0.5] }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
}}
>$36.00</motion.span>
</div></div>
);
}
Onboarding
5 animationsMotion that points at the next thing — the one place overshoot earns its keep.
Onboarding motion directs attention to the next thing, which makes it the one place a deliberate overshoot earns its keep — the eye follows the thing that moves furthest. Everywhere else in this library, bouncy would be decoration; here it is the message.
Welcome card
on first loadOvershoots on purpose. The eye follows whatever travels furthest.
import { motion, type Variants } from "motion/react";
// 4 siblings running one animation 100ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.1,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, y: 16 },
show: {
opacity: [0, 1],
y: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 220, damping: 12, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
},
},
} satisfies Variants;
export function WelcomeCard() {
return (
<motion.div
className="w-full max-w-[320px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex flex-col gap-3">
<motion.div
className="size-9 rounded-lg bg-primary/20"
variants={item}
></motion.div>
<motion.span
className="text-[16px] font-semibold text-foreground"
variants={item}
>Welcome</motion.span>
<motion.span
className="text-[12px] leading-relaxed text-muted-foreground"
variants={item}
>Three steps and you are animating.</motion.span>
<motion.span
className="w-fit rounded-lg bg-primary px-3 py-1.5 text-[12px] font-medium text-primary-foreground"
variants={item}
>Start</motion.span>
</div></motion.div>
);
}
Checklist
on loadItems arrive in order, so the list reads as a sequence rather than a menu.
import { motion, type Variants } from "motion/react";
// 3 siblings running one animation 100ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.1,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, x: -8 },
show: {
opacity: [0, 1],
x: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
x: { type: "spring", stiffness: 220, damping: 12, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
},
},
} satisfies Variants;
export function Checklist() {
return (
<motion.div
className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex flex-col gap-2">
<motion.div
className="flex items-center gap-2.5"
variants={item}
>
<div className="size-4 rounded-full border border-primary bg-primary/20"></div>
<span className="text-[12px] text-foreground">Create a project</span>
</motion.div><motion.div
className="flex items-center gap-2.5"
variants={item}
>
<div className="size-4 rounded-full border border-border"></div>
<span className="text-[12px] text-muted-foreground">Paste a component</span>
</motion.div><motion.div
className="flex items-center gap-2.5"
variants={item}
>
<div className="size-4 rounded-full border border-border"></div>
<span className="text-[12px] text-muted-foreground">Export the code</span>
</motion.div>
</div></motion.div>
);
}
Coach tip
on step enteredArrives from the side it is pointing away from, so it does not cover its target.
import { motion, type Variants } from "motion/react";
// 2 siblings running one animation 100ms apart.
// Motion expresses that as variants on the parent, so the children stay
// declarative and any number of them inherit the timing.
const container = {
hidden: { },
show: {
transition: {
staggerChildren: 0.1,
},
},
} satisfies Variants;
const item = {
hidden: { opacity: 0, x: 8 },
show: {
opacity: [0, 1],
x: 0,
transition: {
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
x: { type: "spring", stiffness: 220, damping: 12, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
},
},
} satisfies Variants;
export function CoachTip() {
return (
<motion.div
className="w-full max-w-[260px] rounded-xl border border-border bg-card p-5"
variants={container}
initial="hidden"
animate="show"
><div className="flex flex-col gap-2">
<motion.span
className="text-[12px] font-medium text-foreground"
variants={item}
>Drag the label</motion.span>
<motion.span
className="text-[11px] leading-relaxed text-muted-foreground"
variants={item}
>Every number in this panel is a slider. Try it on x.</motion.span>
</div></motion.div>
);
}
Progress
on step completeThe bar fills, then the label follows — the number confirms what you just saw.
import { motion } from "motion/react";
export function Progress() {
return (
<div className="w-full max-w-[300px] rounded-xl border border-border bg-card p-5"><div className="flex flex-col gap-3">
<motion.div
className="h-1.5 rounded-full bg-border"
initial={{ opacity: 0, x: -16 }}
animate={{ opacity: [0, 1], x: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
x: { type: "spring", stiffness: 220, damping: 12, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
}}
><div className="h-full w-1/3 rounded-full bg-primary"></div></motion.div>
<motion.span
className="text-[11px] text-muted-foreground"
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.1 },
y: { type: "spring", stiffness: 220, damping: 12, mass: 1, delay: 0.1 },
}}
>Step 1 of 3</motion.span>
</div></div>
);
}
Success
on completeThe biggest overshoot in the library, once, at the end. Earned rather than decorative.
import { motion } from "motion/react";
export function Success() {
return (
<div className="w-full max-w-[420px] rounded-xl border border-border bg-card p-5"><div className="flex flex-col items-center gap-2 py-4">
<motion.div
className="size-12 rounded-full border-2 border-primary bg-primary/10"
initial={{ opacity: 0, y: 4, scale: 0.7 }}
animate={{ opacity: [0, 1], y: 0, scale: 1 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
y: { type: "spring", stiffness: 220, damping: 12, mass: 1 },
scale: { type: "spring", stiffness: 220, damping: 12, mass: 1 },
}}
></motion.div>
<motion.span
className="text-[14px] font-medium text-foreground"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.1 },
y: { type: "spring", stiffness: 220, damping: 12, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.1 },
}}
>You are set up</motion.span>
<motion.span
className="text-[11px] text-muted-foreground"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: [0, 1], y: 0 }}
transition={{
opacity: { duration: 0.22, times: [0, 1], ease: "easeOut", delay: 0.2 },
y: { type: "spring", stiffness: 220, damping: 12, mass: 1, restDelta: 0.01, restSpeed: 0.01, delay: 0.2 },
}}
>That is everything. Go make something.</motion.span>
</div></div>
);
}