Animate React/Next.js UI with the `motion` package (Framer Motion's successor): entrances, stagger, hover/tap feedback, layout and shared-element transitions, AnimatePresence, scroll-linked effects, reduced-motion. Use when adding or fixing animations, transitions or micro-interactions.
Installs into .claude/skills of the current project.
Are you the author of Motion?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tizonai-motion)
---
name: motion
description: "Animate React/Next.js UI with the `motion` package (Framer Motion's successor): entrances, stagger, hover/tap feedback, layout and shared-element transitions, AnimatePresence, scroll-linked effects, reduced-motion. Use when adding or fixing animations, transitions or micro-interactions."
user-invocable: false
allowed-tools:
- Bash(${CLAUDE_PLUGIN_ROOT}/scripts/profile.sh *)
- Read(//${CLAUDE_PLUGIN_ROOT}/**)
- Edit(.hyperui/**)
---
# Motion — animation for React / Next.js
## 0. Before anything
- Resolve the project root with `${CLAUDE_PLUGIN_ROOT}/scripts/profile.sh root` and use it (absolute paths) for `.hyperui/` and for every repo read/write. If `<root>/.hyperui/profile.md` is missing, invoke the `hyperui` skill first (it onboards and routes); otherwise read `profile.md` and `state.md` and never re-ask what they hold. With several roots (`profile.sh roots`), work in the repo the request or the touched file belongs to, with that repo's `.hyperui/` (rules: `${CLAUDE_PLUGIN_ROOT}/references/workspace.md`).
Profile fields used: archetype, `conversation_language`, `design.*`, `tone_notes`; also `.hyperui/design.md`.
- `design.motion` sets the register: `restrained` → only the hero sequence and micro-feedback; `expressive` → add scroll reveals and shared-element transitions. Unset → `restrained`.
`prefers-reduced-motion` is honored by default (recipe H) — never ask whether to.
- Use the durations and easings already in the tokens (`--dur-*`, `--ease-*`) before the defaults below.
- Reply in the conversation language; keep internal reasoning, code and files in English.
- Tone per archetype: non-tech → one or two plain sentences per step; dev → the fact + one line of why; senior → the fact.
**Use the Motion AI Kit when present.** If the official `/motion` skill exists (`<root>/.claude/skills/motion` or
`~/.claude/skills/motion`) or the `motion` MCP is connected, use it for docs and example search, CSS springs and
MotionScore audits (springs and audits need Motion+). This skill stays the taste and choreography layer: the language,
recipes and QA below decide what moves and why. If neither is present, say once that `/hyperui:setup` installs it.
## 1. Install and import
```sh
npm install motion
```
```tsx
"use client"; // required in Next.js App Router for any file that uses motion components/hooks
import { motion, AnimatePresence, MotionConfig, useReducedMotion, useScroll, useTransform, useInView } from "motion/react";
```
- `motion/react` is the current import path. The legacy `framer-motion` package still works and exposes the same API; prefer `motion/react` in new code and do not mix both in one project.
- Vanilla / non-React code: `import { animate, scroll, inView, stagger } from "motion"`.
- `stagger()` from `"motion"` is the standalone helper; inside React variants use `transition: { staggerChildren }` (see recipe A).
- Before using an API not listed here, check https://motion.dev/docs with WebFetch (or the Motion MCP). Do not guess prop names.
## 2. Motion language
- **Durations:** 150–250 ms for micro-feedback (hover, press, toggle), 300–500 ms for entrances,
≤ 800 ms for a hero sequence. Nothing a user waits for takes longer than 500 ms.
- **Easing:** ease-out (`[0.16, 1, 0.3, 1]`) for things entering, ease-in-out for things moving
between places, springs for things the user touches (`stiffness` 300–500, `damping` 25–35).
- **Properties:** animate `transform` (x, y, scale, rotate) and `opacity` only; `clip-path` and
`filter` sparingly. Never animate width/height/top/left directly — use the `layout` prop.
- **Distances:** entrances travel 8–24 px. Scale from 0.96–0.98, never from 0.
- **Stagger:** 40–80 ms between siblings; cap the visible staggered set at ~8 items.
- **One choreography per view.** The hero gets a sequence; everything below gets at most a
quiet reveal. If everything moves, nothing does.
- **Reason, in priority order:** orientation (where did this come from) → feedback (did my action
register) → continuity (same object, new place) → delight. No reason, no animation.
- **Reduced motion is not optional:** `useReducedMotion()` → drop transforms, keep opacity fades,
shorten durations. Wire it once at the root with `MotionConfig` (recipe H).
## 3. Recipes
**A. Variants + stagger for a hero or list**
```tsx
const list = { hidden: {}, show: { transition: { staggerChildren: 0.06, delayChildren: 0.1 } } };
const item = { hidden: { opacity: 0, y: 16 }, show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] } } };
<motion.ul variants={list} initial="hidden" animate="show">
{items.map((it) => <motion.li key={it.id} variants={item}>{it.label}</motion.li>)}
</motion.ul>
```
**B. Button with spring feedback**
```tsx
<motion.button
whileHover={{ scale: 1.02, y: -1 }}
whileTap={{ scale: 0.97 }}
transition={{ type: "spring", stiffness: 400, damping: 28 }}
>
Download
</motion.button>
```
**C. AnimatePresence for swapped content (modal, step, tab panel)**
```tsx
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={step} // a stable, unique key per state is mandatory
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}
>
{content[step]}
</motion.div>
</AnimatePresence>
```
**D. Shared-element tab indicator**
```tsx
{tabs.map((t) => (
<button key={t} onClick={() => setActive(t)} style={{ position: "relative" }}>
{t}
{active === t && (
<motion.span layoutId="tab-indicator" style={{ position: "absolute", inset: "auto 0 0 0", height: 2, background: "var(--accent)" }}
transition={{ type: "spring", stiffness: 500, damping: 35 }} />
)}
</button>
))}
```
**E. Scroll reveal (once, slightly before fully in view)**
```tsx
<motion.section
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-10%" }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
/>
```
**F. Scroll-linked progress bar and parallax**
```tsx
const { scrollYProgress } = useScroll(); // page progress 0..1
<motion.div style={{ scaleX: scrollYProgress, transformOrigin: "0 0", height: 2, background: "var(--accent)", position: "fixed", top: 0, left: 0, right: 0 }} />
const ref = useRef(null);
const { scrollYProgress: p } = useScroll({ target: ref, offset: ["start end", "end start"] });
const y = useTransform(p, [0, 1], [-40, 40]); // parallax drift in px
<motion.img ref={ref} style={{ y }} src="/hero.png" alt="" />
```
**G. Reduced-motion-safe variants**
```tsx
export function useMotionSafe() {
const reduce = useReducedMotion();
const item = reduce
? { hidden: { opacity: 0 }, show: { opacity: 1, transition: { duration: 0.2 } } }
: { hidden: { opacity: 0, y: 16 }, show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] } } };
const list = { hidden: {}, show: { transition: { staggerChildren: reduce ? 0 : 0.06 } } };
return { item, list, reduce };
}
```
**H. Root config (do this once, in the app layout's client provider)**
```tsx
<MotionConfig reducedMotion="user" transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}>
{children}
</MotionConfig>
```
`reducedMotion="user"` makes Motion skip transform/layout animations automatically when the OS setting is on; recipe G is for cases where you want explicit control of the fallback.
## 4. Performance and pitfalls
- Let Motion manage `will-change`; do not sprinkle it in CSS — it costs memory on every element.
- Do not animate hundreds of nodes individually; animate the container, or virtualize and only
animate what is visible (`whileInView`).
- Hydration flicker on mount: pass `initial={false}` to `AnimatePresence` or to the component so
the first paint is the final state.
- SSR: `motion.*` components render their static markup on the server; the `initial` state is
applied as inline style, so a hidden `initial` means the element is invisible until JS runs.
Use it only on elements that are fine being hidden briefly, never on the main heading/CTA
without a short duration.
- `AnimatePresence` needs stable, unique `key`s on direct children; without them exit animations
never run.
- `layout` animations inside scroll containers: mark the scroller with `layoutScroll`; for
independent layout groups use `LayoutGroup` (verify on motion.dev).
- Images and video inside `layout` elements distort during the transition; put `layout` on a wrapper and keep the media `layout="position"`.
- Test with CPU throttling 4× and on a real phone; if anything drops frames, remove the animation rather than tuning it for an hour.
## 5. QA checklist
1. OS "Reduce motion" on → no transforms, only short fades; nothing becomes unreachable.
2. Every interactive element gives feedback within 150 ms on hover and tap.
3. No animation blocks reading or clicking; no entrance runs longer than 500 ms except the hero.
4. 60 fps at 4× CPU throttle on the heaviest page; no layout thrash in the Performance panel.
5. `AnimatePresence` exits actually play (keys are stable), and nothing flickers on first paint.
6. **Seen in a browser, with evidence** (`${CLAUDE_PLUGIN_ROOT}/skills/design/references/browser-verify.md`): capture the choreography (a short GIF with Claude in Chrome when available) AND the reduced-motion path at 360 and 1280, recorded under `## Browser evidence` in `design.md`; no browser tool → say so, never "feels smooth".
## 6. Close the turn
When a choreography is settled (what moves, durations, easings), write it to `.hyperui/design.md` (motion section),
the next step to `state.md`, and one ADR-lite line to `decisions.md` (`date · decision · why · source`).
## Sources
Cite only URLs that are listed in a skill/reference or that you opened this session; never construct or guess a URL.
Verify API names before use. Canonical references: [`references/sources.md`](references/sources.md).
Motion AI Kit (skill + MCP, free vs Motion+): https://motion.dev/docs/ai-kit · install: https://motion.dev/docs/ai-kit-install