Give a Next.js 16 App Router app ONE motion system: a token layer (durations, easings, springs) plus a library of tokenized micro-interactions — entrance/exit, stagger, layout, modal/dropdown/toast/accordion, hover, feedback and route/page transitions. Four modes: Setup (scaffold `lib/motion/` from the DESIGN.md motion block), Apply, Audit (find ad-hoc or hardcoded motion), Refine (swap hardcoded values for tokens). Use when the user says "add a transition", "animate this", "aggiungi una tran...
Installs into .claude/skills of the current project.
Are you the author of Transitions?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/lukedj78-transitions)
---
name: transitions
description: 'Give a Next.js 16 App Router app ONE motion system: a token layer (durations, easings, springs) plus a library of tokenized micro-interactions — entrance/exit, stagger, layout, modal/dropdown/toast/accordion, hover, feedback and route/page transitions. Four modes: Setup (scaffold `lib/motion/` from the DESIGN.md motion block), Apply, Audit (find ad-hoc or hardcoded motion), Refine (swap hardcoded values for tokens). Use when the user says "add a transition", "animate this", "aggiungi una transizione / animazione", "page transition", "stagger these cards", "make the modal open nicely", or "audit the motion in this codebase". Refuses outside Next.js 16 web (`stack.framework` next/monorepo). Not for: React Native motion (use `rn-animations-gestures`), installing the Motion runtime itself (use `module-add motion`), or defining the visual palette/type (that is DESIGN.md + design-md-to-app).'
---
# transitions — one tokenized motion system for a Next.js 16 app
This skill governs **how a web app moves**. Like `state-discipline` for state and `forms` for forms, it routes every animation through one shared, token-driven system so the product has a single, consistent motion feel — instead of scattered `duration-[237ms]` magic numbers and hand-tuned cubic-beziers that drift page to page.
> **Inspired by [transitions.dev](https://transitions.dev/) (Jakub Antalík)** — a curated library that teaches agents about product motion. This is **our own** take: token-driven, wired to the DESIGN.md contract and our Next.js 16 + Motion + `tw-animate-css` stack. Not a fork, not an install of their package — we credit the idea and build our own.
## When this skill applies
- The user says "add a transition / animation", "animate this", "aggiungi una transizione", "make the modal open nicely", "stagger these cards", "page transition between routes".
- You're about to write an inline `transition:` / `animate={{…}}` / `@keyframes` / a Tailwind `duration-[Xms]` arbitrary value.
- The user asks to **audit** a codebase for ad-hoc motion.
- A component needs entrance/exit, hover, feedback, layout, or route motion.
If the effect genuinely needs JS physics (spring, layout/shared-element, drag/gesture, scroll choreography) and the Motion runtime isn't wired, this skill **routes to `module-add motion`** first, then applies the tokenized pattern on top.
## Contract
Follows the dev-flow contract — see `references/contracts.md`. Key facts:
- Reads `meta.json#stack.framework`, `stack.nextjs_version`, `stack.motion`. For monorepo, reads `stack.monorepo.web.*` and operates in `apps/web/`.
- **Refuses** if `stack.framework ∉ {"next", "monorepo"}` or `stack.nextjs_version != "16"`. Principles transfer to other React setups but the View-Transitions/RSC rungs don't — refuse rather than mis-apply.
- Records `meta.json#stack.motion` (see block below) + appends `history` per run.
- Does **not** bump `phase`. Horizontal capability — invoke any time.
## Companion skills — what this owns vs reuses
- **`module-add motion`** — installs the **Motion** runtime (`motion/react`, `components/motion/*`, `lib/motion-config.ts`). This skill sits *above* it: it owns the token layer + the curated library + the discipline; it never re-installs the runtime, it routes there when Tier 3 is needed.
- **`tw-animate-css`** — already shipped by shadcn; it's this skill's **Tier 0** engine (fade/slide/scale, zero JS; it ships no shimmer — `references/tw-animate-css.md`). Reuse it, don't reinvent.
- **`design-md-to-app`** — owns DESIGN.md and the visual tokens. This skill reads the **`motion` block** of DESIGN.md for the *values*; if absent, it scaffolds sane defaults and writes them back (DESIGN.md stays the source of truth for values).
- **anti-slop fallbacks** (`design-md-to-app/references/anti-slop-fallbacks.md`) — the "animate only transform/opacity", hardware-acceleration, content-shaped-skeleton, CSS-stagger rules live there. This skill enforces them; it doesn't duplicate them.
- **`rn-animations-gestures`** — the **mobile** counterpart (Reanimated + Gesture Handler). This skill is web-only.
## Step 0 — should it animate at all? (run this before the ladder)
The ladder's lowest rung is Tier 0, but **the cheapest answer is no animation**, and the question has
a table: **100+ times/day — a keyboard shortcut, the command palette, core navigation — gets no
animation, ever.** Tens of times a day gets near-imperceptible or nothing. Occasional (modals,
drawers, toasts) is where the ladder applies. Rare and first-time moments are where the delight
budget lives. Then name the purpose — feedback, spatial consistency, state indication, preventing a
jarring change, explanation, or delight at the rare tier — or don't build it.
The full gate, the exact curves and durations, the physicality and gesture rules, and the checks to
name when feel cannot be judged from code are in **`references/animation-standards.md`**. Read it
before writing a transition, and before approving one.
## The technique ladder — cheapest tier that does the job
Reach for the **lowest** tier that achieves the effect. Higher tiers cost bundle size, force `"use client"`, or add JS the interaction doesn't need.
| Tier | Engine | Use for | Cost |
|---|---|---|---|
| **0** | Tailwind + `tw-animate-css` | enter/exit fade·slide·scale, hover lift, CSS stagger (`animationDelay`), skeleton pulse (Tailwind's `animate-pulse`, opacity only — never a moving gradient sweep) | zero JS, RSC-safe |
| **1** | Hand-written CSS `transition` / `@keyframes` (tokened) | bespoke feedback (success check draw, error shake, number pop), state-swap crossfades | zero JS, RSC-safe |
| **2** | **View Transitions API** | route/page transitions (Next 16 App Router), same-document DOM swaps (list reorder, tab underline) | tiny JS, mostly RSC-safe |
| **3** | **Motion** (`motion/react`) | spring physics, layout / shared-element, drag & gesture, scroll-driven choreography (`useScroll`/`useTransform`) | ~40kb, forces `"use client"` → routes to `module-add motion` |
| **4** | **WebGPU shaders** (`vgpu`) | raymarching, fluid, volumetric light, real-time generative texture — when the effect **is** the artefact | a GPU device, a compile step, a render loop and a battery — see `vgpu-shaders` |
**On the Tier-2/Tier-3 seam sit Motion's two view-transition APIs** — imperative `animateView(update)` and,
since `motion@13.4.0`, the `<AnimateView>` component (`motion/react-animate-view`). Both choreograph the
browser's View Transitions instead of replacing them, so they stay cheaper than Tier 3 layout animation.
⚠️ `<AnimateView>` renders React's `ViewTransition` and **throws at runtime** — *"AnimateView requires
React 19.3 or later."* — while `motion`'s peer range still accepts React 18/19, so npm installs it on a
19.2 project and it breaks in the browser. Check `react`'s version first; ours are on 19.2.4. Details,
props and the reduced-motion duty (the docs never mention it): `references/motion-library.md`.
**Borrowing motion instead of inventing it — [React Bits](https://reactbits.dev).** 202 animated components as a
shadcn registry (`@react-bits`, MIT + Commons Clause: usable in a product, **never republished as components**).
Its `/c/micro` set (30 micro-interactions) is Motion-based and calls `useReducedMotion()` itself. Read it as a
**cookbook**: most of those items animate a pattern shadcn already ships (Spring Check → `Checkbox`, Squish Switch
→ `Switch`, Swipe Toast → `Sonner`, four buttons → `Button`), so golden rule 3 says port the timings and gestures
onto **our** primitive through `lib/motion/tokens.ts` — not install a second one. For the few with no primitive
behind them, install through `registry-intake` (never a bare `shadcn add`), take the `TS-TW` variant, and expect
two findings: the items declare `motion@^12` against our 13, and their long arbitrary-value class lists raise the
design-lint cap. Details, the full micro list and the primitive map: `references/react-bits.md`. Other catalogues worth *looking* at under the same two rules — a MIT page of ~24 named loaders, Watermelon UI's MIT `watermelon` registry (1,178 items, and **always the `-base` twin**, which is the same component written in theme tokens instead of hex and `text-[15px]`), and unlumen UI's paid `@unlumen-ui` registry: `references/motion-catalogues.md`.
**Never jump to Tier 3 for a fade.** A `<Suspense>` fallback, a hover lift, a dropdown open — all Tier 0/1. Reserve Motion for interactions that genuinely need physics or layout animation.
**And Tier 4 is not the next step up from Tier 3 — it is a different order of cost.** A shader is right
when the visual *is* the product (a hero that exists to be looked at); it is wrong as decoration on a
screen someone keeps open all day. ⚠️ vgpu's own docs never mention `prefers-reduced-motion`, so rule 2
below is entirely yours to honour there — freeze the clock (`advance(0)`) and hold the first frame
rather than hiding the canvas. Route to **`vgpu-shaders`** before writing any WGSL. Once a project is
already on both Motion and vgpu, `motion/vgpu`'s `vgpuEffect` (Motion 13.2.0+) drives shader uniforms
and scene params with Motion's spring/gesture engine instead of a hand-rolled render loop — see
`vgpu-shaders` §Animating once you're here. That is an implementation detail *inside* Tier 4, not a
reason to reach for it sooner.
## Non-negotiables (the discipline)
1. **Tokens, not magic numbers.** Every duration/easing/spring/distance comes from `lib/motion/tokens.ts`. No inline `duration-[237ms]`, no ad-hoc `cubic-bezier(...)` in components.
2. **`prefers-reduced-motion` always.** Every transition ships a reduced-motion fallback (opacity-only or instant). Tier 0/1 use the `motion-reduce:` Tailwind variant or a media query; Tier 3 uses `useReducedMotion()`. A transition without a reduced-motion path is incomplete. **This rule is machine-checkable**: `shadscan`'s `animations-respect-reduced-motion` verifies it against the built app — run that gate before shipping instead of trusting a grep.
3. **Gate hover motion on capability.** Touch has no hover, so the browser fakes one: the first tap applies `:hover` and **leaves it stuck** until the user taps elsewhere — a button that scales on hover stays scaled after a tap. Tailwind v4's `hover:` variant already compiles to `@media (hover: hover)`, so utility classes are covered; **hand-written `:hover` CSS is not** and needs `@media (hover: hover) and (pointer: fine)`. Touch gets its feedback from `:active`, which needs no gating.
4. **Animate only `transform` and `opacity`** (+ `filter` sparingly). Never animate `width`/`height`/`top`/`left`/`box-shadow` — layout thrash. Use `transform: scale/translate` and, for layout, Tier 2/3.
5. **Don't force `"use client"` for motion that doesn't need it.** Prefer Tiers 0–2 to keep Server Components server-rendered.
6. **Motion has meaning, and Step 0 is how you check.** Entrance ≠ decoration: it should clarify hierarchy, direction, or causality. Name the purpose in one word or cut it — and if the element is seen 100+ times a day, cut it regardless.
7. **Primitives keep their own motion (golden rule 3).** `Dialog`, `Sheet`, `Popover`, `Tooltip`, `Accordion`, `Sonner` already animate open/close with their state. Before writing a transition, search `components/ui/` for the primitive that owns the pattern and use it; tune its motion through tokens and `cva`/class variants, never by re-implementing the component or forking its behaviour to hang an animation on it.
## `lib/motion/tokens.ts` — the token layer
Setup mode scaffolds this from the DESIGN.md `motion` block (or defaults). Illustrative shape:
```ts
// Durations (ms) — the only durations the app uses.
export const duration = { instant: 0, fast: 120, base: 200, slow: 320, slower: 480 } as const;
// Easings — named curves; components reference these, never raw beziers.
export const ease = {
standard: "cubic-bezier(0.2, 0, 0, 1)", // enter AND exit — both are ease-out
emphasized: "cubic-bezier(0.3, 0, 0, 1)", // hero, page
inOut: "cubic-bezier(0.77, 0, 0.175, 1)", // movement *on* screen: a morph, A→B
} as const;
// There is deliberately no `exit` curve. It used to be `cubic-bezier(0.4, 0, 1, 1)`,
// an ease-in, and an exit on the critical path (a dialog the user closed to reach
// what is behind it) is latency — an accelerate curve holds it at near-full opacity
// through the half of the duration that matters. See references/animation-standards.md.
// Distances (px) — how far things slide in.
export const distance = { sm: 4, md: 8, lg: 16 } as const;
// Spring presets (Tier 3 only) — mirrors module-add motion's lib/motion-config.ts if present.
export const spring = {
soft: { type: "spring", stiffness: 300, damping: 30 },
snappy: { type: "spring", stiffness: 500, damping: 32 },
} as const;
```
CSS consumers get the same values as custom properties (`--motion-duration-base`, `--motion-ease-standard`, …) so Tailwind arbitrary values reference `var(--motion-*)` instead of literals. The full token→CSS-var bridge and the reduced-motion wiring are in `references/motion-library.md`.
## Modes
Read state, then pick a mode:
1. Read `meta.json#stack`. Refuse if not Next 16 (web). Check whether `lib/motion/tokens.ts` exists.
2. If the requested effect is Tier 3 and Motion isn't installed (`package.json` has no `motion`/`framer-motion`), **route to `module-add motion`**, then continue.
3. Choose the mode.
### Setup (first run)
Scaffold `lib/motion/` : `tokens.ts` (from DESIGN.md `motion` block or defaults), the CSS-var bridge in the global stylesheet, and `transitions.ts` (the tokenized variant/classname library — the curated set from `references/motion-library.md`). Idempotent: detect existing files, offer to extend, never double-write. Record `stack.motion`.
### Apply (add a transition)
Pick the **best-fit** transition from the library for the component/context, at the **lowest viable tier**, propose it in one line with the rationale (which tier + why), then wire it using the tokens. Always include the reduced-motion fallback. Never introduce a new magic number — if the library lacks a fit, add a *tokenized* entry to `transitions.ts` rather than inlining.
### Audit (find ad-hoc motion)
Run `python scripts/scan_motion.py <root>` for a first-pass signal, then verify each hit in code. The scan reads
source; to prove non-negotiable ② on the running page, run `app-verification`'s render probe
(`node <skills>/app-verification/scripts/render_probe.mjs <url>`): its `reduced-hidden` finding lists text that is
visible normally and stays invisible with reduced motion — an entrance with no reduced-motion path. Report un-tokenized durations/easings, `duration-[Xms]`/`ease-[…]` Tailwind arbitraries, inline `cubic-bezier`, `@keyframes` animating layout props, transitions with no `prefers-reduced-motion` path, and Tier-3 usage where Tier 0/1 would do. See `references/audit-recipe.md`. The scan is a **signal, not a verdict**.
### Refine (swap to tokens)
In a given file, replace hardcoded durations/easings with the nearest token (round to the token scale) and add any missing reduced-motion fallback. Every change is a reviewable diff.
## `meta.json#stack.motion` block
```jsonc
"motion": {
"runtime": "tw-animate-css" | "motion" | "both", // Tier 0 only, or Motion also wired
"library": "motion" | "framer-motion" | null, // the Tier-3 package, when one is wired
"tokens": true, // lib/motion/tokens.ts scaffolded
"view_transitions": true | false, // route transitions enabled
"last_audit_at": "<ISO>" | null
}
```
⚠️ **This key is shared with `module-add motion`, and the two used to write
different *shapes* into it** — that skill wrote the bare string `"motion"`, this
one writes the object above, so whichever ran second silently destroyed what the
first recorded. Settled in favour of the object, because it can carry everything
the string said: `module-add motion` now writes `stack.motion.library` and
`runtime`, leaving `tokens` / `view_transitions` to this skill. If you find a
`meta.json` with `"motion": "motion"`, it predates the fix — replace it with
`{ "runtime": "motion", "library": "motion", "tokens": false, "view_transitions": false, "last_audit_at": null }`.
## Definition of Done
- **Setup**: `lib/motion/tokens.ts` + CSS-var bridge + `transitions.ts` exist; `stack.motion.tokens = true`; values trace to the DESIGN.md `motion` block.
- **Apply**: the effect uses the lowest viable tier, references tokens (no magic numbers), and has a `prefers-reduced-motion` fallback.
- **Audit/Refine**: report lists only verified hits; refines land as diffs; a re-run is a no-op for already-tokenized code.
- Script green: `cd transitions/scripts && python3 -m unittest test_scan_motion`.
## What this skill does NOT do
- **Doesn't install the Motion runtime** — that's `module-add motion` (this skill routes there for Tier 3).
- **Doesn't define the palette/type** — DESIGN.md + `design-md-to-app` own visual tokens; this owns *motion* tokens.
- **Doesn't do React Native** — use `rn-animations-gestures`.
- **Doesn't bump `phase`.**
## Reference files
- `references/motion-catalogues.md` — where to look for motion and what looking may turn into: the loader vocabulary at loading.daniasyrofi.com (MIT, and why a skeleton still beats a spinner for content), and unlumen UI (`@unlumen-ui` shadcn registry, Free/Pro, **$119 one-time** or $69/year — with its second component namespace and the routing to `vgpu-shaders` / `animated-icons`).
- `references/react-bits.md` — React Bits as a motion source: licence, the `TS-TW` rule, the micro→primitive map golden rule 3 imposes, the Motion-12 and arbitrary-value traps, and where its other categories land on the ladder.
- `references/motion-library.md` — the curated, tokenized transition library: each entry → tier, tokens used, code snippet, and its `prefers-reduced-motion` fallback. Grouped enter/exit · toggle · hover · feedback · layout · route.
- `references/tw-animate-css.md` — the **Tier-0 engine how-to** (doc-grounded): the `animate-in`/`animate-out` class set and modifiers, `data-[state]` composition, the CSS variables to point at `lib/motion/tokens.ts`, and the `motion-reduce:` pattern. Read it before writing Tier-0 classes — don't guess class names.
- `references/audit-recipe.md` — "audit my codebase for ad-hoc motion" recipe (patterns, verify steps, refine order).
- `references/contracts.md` — the `.workflow/` dev-flow contract (vendored).