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 animations

Data arriving, without making anyone wait to read it.

The profile
springcrispstiffness 480 · damping 34 · ζ 0.78
travelshort8px
staggertight60ms between siblings

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 load

Three numbers land left to right, tight enough to read as one row.

Revenue $48.2k
Active 1,284
Churn 1.8%
settles ~770 ms
3 animated · complete, in your markup
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 load

Rows arrive in order. Nothing overshoots, because a moving row is unreadable.

Acme Corp 62 users
Globex 93 users
Initech 124 users
Umbrella 155 users
settles ~800 ms
4 animated · complete, in your markup
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 load

Bars grow from the axis rather than fading — height IS the data.

settles ~700 ms
7 animated · complete, in your markup
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>
  );
}

Sidebar nav

on load

Items slide in from the rail edge, the direction they conceptually come from.

Overview
Customers
Billing
Settings
settles ~830 ms
4 animated · complete, in your markup
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, x: -8 },
  show: {
    opacity: [0, 1],
    x: 0,
    transition: {
      opacity: { duration: 0.22, times: [0, 1], ease: "easeOut" },
      x: { type: "spring", stiffness: 480, damping: 34, mass: 1, restDelta: 0.01, restSpeed: 0.01 },
    },
  },
} satisfies Variants;

export function SidebarNav() {
  return (
    <motion.div
      className="w-full max-w-[220px] rounded-xl border border-border bg-card p-5"
      variants={container}
      initial="hidden"
      animate="show"
    ><div className="flex flex-col gap-1">
        <motion.div
          className="rounded-md px-3 py-2 text-[12px] bg-secondary text-foreground"
          variants={item}
        >Overview</motion.div><motion.div
          className="rounded-md px-3 py-2 text-[12px] text-muted-foreground"
          variants={item}
        >Customers</motion.div><motion.div
          className="rounded-md px-3 py-2 text-[12px] text-muted-foreground"
          variants={item}
        >Billing</motion.div><motion.div
          className="rounded-md px-3 py-2 text-[12px] text-muted-foreground"
          variants={item}
        >Settings</motion.div>
      </div></motion.div>
  );
}

Empty state

when a list is empty

The slowest thing in this kit, deliberately — nothing is waiting on it.

No customers yet They will appear here as they sign up.
settles ~790 ms
3 animated · complete, in your markup
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 animations

Motion with room to breathe, for people deciding whether to care.

The profile
springgentlestiffness 120 · damping 20 · ζ 0.91
travellong28px
staggerloose160ms between siblings

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 load

Badge, headline, body, buttons — in reading order, which is the only order.

New

Ship motion you can read.

Design it by touching it, then take the code with you.

Start free See an export
settles ~1650 ms
4 animated · complete, in your markup
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 view

A grid reveals along the reading diagonal, not row by row.

Timeline
Springs
Export
Parity
settles ~1490 ms
4 animated · complete, in your markup
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 view

Fades only. Logos that slide look like they are falling off the page.

settles ~560 ms
5 animated · complete, in your markup
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 view

The quote leads; the attribution follows, because that is the reading order.

"The export is the product. I read it before I trusted it."

Staff engineer, somewhere real
settles ~1330 ms
2 animated · complete, in your markup
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 view

The price arrives last. Everything before it is context for the number.

Pro $12/mo Every dialectUnlimited exportThe whole library
settles ~1810 ms
5 animated · complete, in your markup
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 animations

Confirmation you cannot miss, on pages where a dropped signal costs money.

The profile
springsnappystiffness 260 · damping 24 · ζ 0.74
travelbase16px
staggerbase100ms between siblings

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 load

Cards arrive together enough to read as a set, not as a queue.

Item 1 $27
Item 2 $36
Item 3 $45
settles ~1080 ms
3 animated · complete, in your markup
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 cart

The fastest confirmation in the set. Doubt here costs a sale.

Added to cart Checkout
settles ~980 ms
2 animated · complete, in your markup
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 cart

The panel arrives, then its contents — so the box is never empty on screen.

Your cart
Item 1 $27
Item 2 $36
settles ~1080 ms
3 animated · complete, in your markup
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 change

Progress fills forward only. A checkout bar that animates backwards reads as an error.

Cart
Details
Pay
settles ~1050 ms
3 animated · complete, in your markup
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 applied

The new price arrives; the old one stays put and fades. Movement means change.

$27.00 $36.00
settles ~850 ms
2 animated · complete, in your markup
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 animations

Motion that points at the next thing — the one place overshoot earns its keep.

The profile
springbouncystiffness 220 · damping 12 · ζ 0.40
travelbase16px
staggerbase100ms between siblings

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 load

Overshoots on purpose. The eye follows whatever travels furthest.

Welcome Three steps and you are animating. Start
settles ~1940 ms
4 animated · complete, in your markup
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 load

Items arrive in order, so the list reads as a sequence rather than a menu.

Create a project
Paste a component
Export the code
settles ~1800 ms
3 animated · complete, in your markup
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 entered

Arrives from the side it is pointing away from, so it does not cover its target.

Drag the label Every number in this panel is a slider. Try it on x.
settles ~1700 ms
2 animated · complete, in your markup
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 complete

The bar fills, then the label follows — the number confirms what you just saw.

Step 1 of 3
settles ~1640 ms
2 animated · complete, in your markup
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 complete

The biggest overshoot in the library, once, at the end. Earned rather than decorative.

You are set up That is everything. Go make something.
settles ~1840 ms
3 animated · complete, in your markup
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>
  );
}