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 (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/shimmer, zero JS). 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.
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 shimmer |
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 |
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)
- 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.
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.
- 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.
- Don't force
"use client" for motion that doesn't need it. Prefer Tiers 0–2 to keep Server Components server-rendered.
- Motion has meaning. Entrance ≠ decoration: it should clarify hierarchy, direction, or causality. If it doesn't, cut it.
lib/motion/tokens.ts — the token layer
Setup mode scaffolds this from the DESIGN.md motion block (or defaults). Illustrative shape:
// 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/exit, most UI
emphasized: "cubic-bezier(0.3, 0, 0, 1)", // hero, page
exit: "cubic-bezier(0.4, 0, 1, 1)", // leaving the screen
} as const;
// 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:
- Read
meta.json#stack. Refuse if not Next 16 (web). Check whether lib/motion/tokens.ts exists.
- 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.
- 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. 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
"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-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).
1---2name: transitions3description: 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).4---56# transitions — one tokenized motion system for a Next.js 16 app78This 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.910> **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.1112## When this skill applies1314- The user says "add a transition / animation", "animate this", "aggiungi una transizione", "make the modal open nicely", "stagger these cards", "page transition between routes".15- You're about to write an inline `transition:` / `animate={{…}}` / `@keyframes` / a Tailwind `duration-[Xms]` arbitrary value.16- The user asks to **audit** a codebase for ad-hoc motion.17- A component needs entrance/exit, hover, feedback, layout, or route motion.1819If 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.2021## Contract2223Follows the dev-flow contract — see `references/contracts.md`. Key facts:2425- Reads `meta.json#stack.framework`, `stack.nextjs_version`, `stack.motion`. For monorepo, reads `stack.monorepo.web.*` and operates in `apps/web/`.26- **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.27- Records `meta.json#stack.motion` (see block below) + appends `history` per run.28- Does **not** bump `phase`. Horizontal capability — invoke any time.2930## Companion skills — what this owns vs reuses3132- **`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.33- **`tw-animate-css`** — already shipped by shadcn; it's this skill's **Tier 0** engine (fade/slide/scale/shimmer, zero JS). Reuse it, don't reinvent.34- **`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).35- **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.36- **`rn-animations-gestures`** — the **mobile** counterpart (Reanimated + Gesture Handler). This skill is web-only.3738## The technique ladder — cheapest tier that does the job3940Reach for the **lowest** tier that achieves the effect. Higher tiers cost bundle size, force `"use client"`, or add JS the interaction doesn't need.4142| Tier | Engine | Use for | Cost |43|---|---|---|---|44| **0** | Tailwind + `tw-animate-css` | enter/exit fade·slide·scale, hover lift, CSS stagger (`animationDelay`), skeleton shimmer | zero JS, RSC-safe |45| **1** | Hand-written CSS `transition` / `@keyframes` (tokened) | bespoke feedback (success check draw, error shake, number pop), state-swap crossfades | zero JS, RSC-safe |46| **2** | **View Transitions API** | route/page transitions (Next 16 App Router), same-document DOM swaps (list reorder, tab underline) | tiny JS, mostly RSC-safe |47| **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` |4849| **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` |5051**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.5253**And Tier 4 is not the next step up from Tier 3 — it is a different order of cost.** A shader is right54when the visual *is* the product (a hero that exists to be looked at); it is wrong as decoration on a55screen someone keeps open all day. ⚠️ vgpu's own docs never mention `prefers-reduced-motion`, so rule 256below is entirely yours to honour there — freeze the clock (`advance(0)`) and hold the first frame57rather than hiding the canvas. Route to **`vgpu-shaders`** before writing any WGSL. Once a project is58already on both Motion and vgpu, `motion/vgpu`'s `vgpuEffect` (Motion 13.2.0+) drives shader uniforms59and scene params with Motion's spring/gesture engine instead of a hand-rolled render loop — see60`vgpu-shaders` §Animating once you're here. That is an implementation detail *inside* Tier 4, not a61reason to reach for it sooner.6263## Non-negotiables (the discipline)64651. **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.662. **`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.673. **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.684. **Don't force `"use client"` for motion that doesn't need it.** Prefer Tiers 0–2 to keep Server Components server-rendered.695. **Motion has meaning.** Entrance ≠ decoration: it should clarify hierarchy, direction, or causality. If it doesn't, cut it.7071## `lib/motion/tokens.ts` — the token layer7273Setup mode scaffolds this from the DESIGN.md `motion` block (or defaults). Illustrative shape:7475```ts76// Durations (ms) — the only durations the app uses.77export const duration = { instant: 0, fast: 120, base: 200, slow: 320, slower: 480 } as const;78// Easings — named curves; components reference these, never raw beziers.79export const ease = {80 standard: "cubic-bezier(0.2, 0, 0, 1)", // enter/exit, most UI81 emphasized: "cubic-bezier(0.3, 0, 0, 1)", // hero, page82 exit: "cubic-bezier(0.4, 0, 1, 1)", // leaving the screen83} as const;84// Distances (px) — how far things slide in.85export const distance = { sm: 4, md: 8, lg: 16 } as const;86// Spring presets (Tier 3 only) — mirrors module-add motion's lib/motion-config.ts if present.87export const spring = {88 soft: { type: "spring", stiffness: 300, damping: 30 },89 snappy: { type: "spring", stiffness: 500, damping: 32 },90} as const;91```9293CSS 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`.9495## Modes9697Read state, then pick a mode:98991. Read `meta.json#stack`. Refuse if not Next 16 (web). Check whether `lib/motion/tokens.ts` exists.1002. 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.1013. Choose the mode.102103### Setup (first run)104Scaffold `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`.105106### Apply (add a transition)107Pick 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.108109### Audit (find ad-hoc motion)110Run `python scripts/scan_motion.py <root>` for a first-pass signal, then verify each hit in code. 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**.111112### Refine (swap to tokens)113In 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.114115## `meta.json#stack.motion` block116117```jsonc118"motion": {119 "runtime": "tw-animate-css" | "motion" | "both", // Tier 0 only, or Motion also wired120 "library": "motion" | "framer-motion" | null, // the Tier-3 package, when one is wired121 "tokens": true, // lib/motion/tokens.ts scaffolded122 "view_transitions": true | false, // route transitions enabled123 "last_audit_at": "<ISO>" | null124}125```126127⚠️ **This key is shared with `module-add motion`, and the two used to write128different *shapes* into it** — that skill wrote the bare string `"motion"`, this129one writes the object above, so whichever ran second silently destroyed what the130first recorded. Settled in favour of the object, because it can carry everything131the string said: `module-add motion` now writes `stack.motion.library` and132`runtime`, leaving `tokens` / `view_transitions` to this skill. If you find a133`meta.json` with `"motion": "motion"`, it predates the fix — replace it with134`{ "runtime": "motion", "library": "motion", "tokens": false, "view_transitions": false, "last_audit_at": null }`.135136## Definition of Done137138- **Setup**: `lib/motion/tokens.ts` + CSS-var bridge + `transitions.ts` exist; `stack.motion.tokens = true`; values trace to the DESIGN.md `motion` block.139- **Apply**: the effect uses the lowest viable tier, references tokens (no magic numbers), and has a `prefers-reduced-motion` fallback.140- **Audit/Refine**: report lists only verified hits; refines land as diffs; a re-run is a no-op for already-tokenized code.141- Script green: `cd transitions/scripts && python3 -m unittest test_scan_motion`.142143## What this skill does NOT do144145- **Doesn't install the Motion runtime** — that's `module-add motion` (this skill routes there for Tier 3).146- **Doesn't define the palette/type** — DESIGN.md + `design-md-to-app` own visual tokens; this owns *motion* tokens.147- **Doesn't do React Native** — use `rn-animations-gestures`.148- **Doesn't bump `phase`.**149150## Reference files151152- `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.153- `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.154- `references/audit-recipe.md` — "audit my codebase for ad-hoc motion" recipe (patterns, verify steps, refine order).155- `references/contracts.md` — the `.workflow/` dev-flow contract (vendored).