git:20260526.876125d to v1.0.0
330 added, 77 removed. Audit A to A.
---
name: vercel-react-view-transitions
- description: React View Transitions and Next.js App Router route continuity. Use for page or route transitions, shared element morphs, React ViewTransition usage, list-to-detail navigation polish, and navigation motion across admin, donor, or missionary surfaces — without fighting motion/react.
+ description: Guide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view transitions, `startViewTransition`, `ViewTransition`, transition types, or asks about animating between UI states in React without third-party animation libraries.
+ license: MIT
metadata:
- owner: "skills-steward"
- last_updated: 2026-04-04
- status: "active"
- upstream:
- url: "https://skills.sh/vercel-labs/agent-skills/vercel-react-view-transitions"
- repo: "vercel-labs/agent-skills"
- path: "skills/react-view-transitions/SKILL.md"
- ref: "73140fc5b3a214ad3222bcf557b397b3c02d11c1"
- license: "MIT"
+ author: vercel
+ version: "1.0.0"
---
- # Vercel React View Transitions (Asymmetric.al monorepo)
+ # React View Transitions
- Browser-native **View Transitions** coordinated by React’s `<ViewTransition>` (see [React docs](https://react.dev/reference/react/ViewTransition)). In this repo they complement — **do not replace** — `motion/react` (`@asym/lib/motion`, `docs/ai/skills/motion`, `docs/ai/skills/anim`).
+ Animate between UI states using the browser's native `document.startViewTransition`. Declare _what_ with `<ViewTransition>`, trigger _when_ with `startTransition` / `useDeferredValue` / `Suspense`, control _how_ with CSS classes. Unsupported browsers skip animations gracefully.
- ## When to Apply
+ ## When to Animate
- Use this skill when:
+ Every `<ViewTransition>` should communicate a spatial relationship or continuity. If you can't articulate what it communicates, don't add it.
- - Adding or tuning **route-level** continuity inside app shells (`MCShell`, missionary `AppShell`, donor public/dashboard layouts).
- - Adding **shared element** morphs (same stable `name` on source + destination).
- - Reviewing navigation motion so it stays **Maia / Zinc**: quiet, short, opacity-first, subtle scale/translate.
- - Debugging **double animation** (VT + `motion` entrance on the same block).
+ Implement **all** applicable patterns from this list, in this order:
- Do not use this skill when:
+ | Priority | Pattern | What it communicates |
+ | -------- | ---------------------------------- | -------------------------------- |
+ | 1 | **Shared element** (`name`) | "Same thing — going deeper" |
+ | 2 | **Suspense reveal** | "Data loaded" |
+ | 3 | **List identity** (per-item `key`) | "Same items, new arrangement" |
+ | 4 | **State change** (`enter`/`exit`) | "Something appeared/disappeared" |
+ | 5 | **Route change** (layout-level) | "Going to a new place" |
- - The surface is **Payload `/web-studio`** — keep that path free of extra client boundaries (admin already bypasses `MCShell` there).
- - The task is **micro-interaction only** (hover, tap, dialog choreography) — use `motion` + `anim` instead.
- - You need **production guarantees** — upstream Next docs still mark this area experimental; ship behind the repo flag (below).
+ This is an implementation order, not a "pick one" list. Implement every pattern that fits the app. Only skip a pattern if the app has no use case for it.
- ## Repo Integration (source of truth)
+ ### Choosing Animation Style
- | Concern | Location |
- | ------------------------------- | ------------------------------------------------------------------------------------ |
- | Next flag | `experimental.viewTransition: true` in each app `next.config.ts` |
- | Rollout flag | `NEXT_PUBLIC_VIEW_TRANSITIONS_ENABLED` in `@asym/env` + `.env.example` |
- | Capability + naming helpers | `@asym/lib/view-transitions` |
- | Route boundary + shared wrapper | `@asym/ui/components/view-transitions` |
- | Global VT animation classes | `packages/ui/styles/globals.css` (`asym-vt-*`) |
- | Shell insertion | Admin `apps/admin/app/mc-shell.tsx` `<main>` |
- | | Missionary `apps/missionary/components/app-shell.tsx` |
- | | Donor `apps/donor/app/(public)/layout.tsx` + donor-dashboard layout |
- | Page header vs VT | `PageShell` quiets motion when inside route VT (`useWithinViewTransitionRouteLayer`) |
+ | Context | Animation | Why |
+ | --------------------------------------- | -------------------------------------------------- | ---------------------------- |
+ | Hierarchical navigation (list → detail) | Type-keyed `nav-forward` / `nav-back` | Communicates spatial depth |
+ | Lateral navigation (tab-to-tab) | Bare `<ViewTransition>` (fade) or `default="none"` | No depth to communicate |
+ | Suspense reveal | `enter`/`exit` string props | Content arriving |
+ | Revalidation / background refresh | `default="none"` | Silent — no animation needed |
- ## Rollout Rules
+ Reserve directional slides for hierarchical navigation (list → detail) and ordered sequences (prev/next photo, carousel, paginated results). For ordered sequences, the direction communicates position: "next" slides from right, "previous" from left. Lateral/unordered navigation (tab-to-tab) should not use directional slides — it falsely implies spatial depth.
- View Transitions run only when **all** are true:
+ ---
- 1. `NEXT_PUBLIC_VIEW_TRANSITIONS_ENABLED=true` (validated via `@asym/env`).
- 2. Browser supports `document.startViewTransition` (see `clientDocumentSupportsViewTransitions()`).
- 3. User does **not** prefer reduced motion (`useReducedMotion()` from `@asym/lib/motion`).
+ ## Availability
- Otherwise render plain children — no broken navigation, no hydration tricks.
+ - **Next.js:** Do **not** install `react@canary` — the App Router already bundles React canary internally. `ViewTransition` works out of the box. `npm ls react` may show a stable-looking version; this is expected.
+ - **Without Next.js:** Install `react@canary react-dom@canary` (`ViewTransition` is not in stable React).
+ - Browser support: Chromium 111+, Firefox 144+, Safari 18.2+. Graceful degradation on unsupported browsers.
- ## API Surface (do not invent props)
+ ---
- - Import `<ViewTransition>` from **`react`** (Next 16 bundles the compatible React channel). Load types via `import {} from "react/experimental"` in client files that need canary typings.
- - Props include `default`, `enter`, `exit`, `share`, `update`, and optional `name` for shared morphs — see `@types/react/canary.d.ts` in this repo.
- - Prefer **`default="none"`** and opt into specific triggers to avoid noisy cross-fades on every Suspense tick (upstream skill guidance).
- - **`addTransitionType`** exists for typed animations; Next.js-specific automatic transition typing is still evolving — read bundled `.next-docs/.../viewTransition.mdx` before relying on framework wiring.
+ ## Implementation Workflow
- ## Placement Rules
+ When adding view transitions to an existing app, **follow `references/implementation.md` step by step.** Start with the audit — do not skip it. Copy the CSS recipes from `references/css-recipes.md` into the global stylesheet — do not write your own animation CSS.
- 1. **Keep shells stable** — wrap the changing `<main>` / content column only, not `<html>` or full provider trees.
- 2. **`<ViewTransition>` must be the first DOM wrapper** for enter/exit on that subtree (no extra parent `div` above it for those triggers — our `RouteMainViewTransitionBoundary` puts the optional `className` on an inner div **inside** `<ViewTransition>`).
- 3. **Shared elements**: one mounted `name` at a time. Use deterministic builders in `@asym/lib/view-transitions` (e.g. `worker-hero:${id}`).
- 4. **Payload admin**: never wrap `/web-studio` content with the route boundary (already excluded in `MCShell`).
+ ---
- ## Avoiding Double Animation with motion/react
+ ## Core Concepts
- - **Route VT** owns screen-to-screen continuity.
- - **motion/react** owns hovers, presses, `AnimatePresence`, dialogs, staggered cards, feed reactions.
- - If both animate the **same** header block, pick one: `PageShell` already disables its header motion when `useWithinViewTransitionRouteLayer()` is true. Apply the same pattern elsewhere (context lives in `@asym/lib/view-transitions/context`).
+ ### The `<ViewTransition>` Component
- ## Accessibility
+ ```jsx
+ import { ViewTransition } from "react";
- - Respect `prefers-reduced-motion` (wrappers return plain children; CSS `@media (prefers-reduced-motion: reduce)` zeroes VT animations in `globals.css`).
- - Never animate focus rings; do not delay validation or error feedback.
+ <ViewTransition>
+ <Component />
+ </ViewTransition>;
+ ```
- ## Triggers
+ React auto-assigns a unique `view-transition-name` and calls `document.startViewTransition` behind the scenes. Never call `startViewTransition` yourself.
- - Page transitions, route transitions, shared element transitions, React `ViewTransition`, Next.js App Router view-transition work, list/detail navigation polish across `apps/admin`, `apps/donor`, `apps/missionary`.
+ ### Animation Triggers
- ## Workflow
+ | Trigger | When it fires |
+ | ---------- | ------------------------------------------------------------------------------------------------- |
+ | **enter** | `<ViewTransition>` first inserted during a Transition |
+ | **exit** | `<ViewTransition>` first removed during a Transition |
+ | **update** | DOM mutations inside a `<ViewTransition>`. With nested VTs, mutation applies to the innermost one |
+ | **share** | Named VT unmounts and another with same `name` mounts in the same Transition |
- 1. Confirm `experimental.viewTransition` is enabled for the target app.
- 2. Decide if the change needs the **env flag** on in dev (`NEXT_PUBLIC_VIEW_TRANSITIONS_ENABLED=true`).
- 3. Add or reuse **stable transition names** for shared elements.
- 4. Wrap **only** the swapping region with `RouteMainViewTransitionBoundary`.
- 5. Tone down overlapping **motion** on the same surface.
- 6. Verify Payload `/web-studio`, auth redirects, and reduced motion.
+ Only `startTransition`, `useDeferredValue`, or `Suspense` activate VTs. Regular `setState` does not animate.
- ## Checklist
+ ### Critical Placement Rule
- - [ ] Used `@asym/ui` boundaries instead of one-off per-app VT wrappers
- - [ ] Shared element `name` is deterministic and matches on both ends
- - [ ] No VT wrapper around Payload admin or entire provider tree
- - [ ] Reduced motion and flag-off paths tested
- - [ ] No competing `motion` entrance on the same node as route VT
- - [ ] After editing this skill: `bun run skills:sync` && `bun run skills:verify`
+ `<ViewTransition>` only activates enter/exit if it appears **before any DOM nodes**:
- ## References
+ ```jsx
+ // Works
+ <ViewTransition enter="auto" exit="auto">
+ <div>Content</div>
+ </ViewTransition>
- - Upstream provenance: `references/upstream.md`
- - Next.js config note: `.next-docs/01-app/03-api-reference/05-config/01-next-config-js/viewTransition.mdx`
- - Motion stack: `docs/ai/skills/motion/SKILL.md`, `docs/ai/skills/anim/SKILL.md`
+ // Broken — div wraps the VT, suppressing enter/exit
+ <div>
+ <ViewTransition enter="auto" exit="auto">
+ <div>Content</div>
+ </ViewTransition>
+ </div>
+ ```
+
+ ---
+
+ ## Styling with View Transition Classes
+
+ ### Props
+
+ Values: `"auto"` (browser cross-fade), `"none"` (disabled), `"class-name"` (custom CSS), or `{ [type]: value }` for type-specific animations.
+
+ ```jsx
+ <ViewTransition
+ default="none"
+ enter="slide-in"
+ exit="slide-out"
+ share="morph"
+ />
+ ```
+
+ If `default` is `"none"`, all triggers are off unless explicitly listed.
+
+ ### CSS Pseudo-Elements
+
+ - `::view-transition-old(.class)` — outgoing snapshot
+ - `::view-transition-new(.class)` — incoming snapshot
+ - `::view-transition-group(.class)` — container
+ - `::view-transition-image-pair(.class)` — old + new pair
+
+ See `references/css-recipes.md` for ready-to-use animation recipes.
+
+ ---
+
+ ## Transition Types
+
+ Tag transitions with `addTransitionType` so VTs can pick different animations based on context. Call it multiple times to stack types — different VTs in the tree react to different types:
+
+ ```jsx
+ startTransition(() => {
+ addTransitionType("nav-forward");
+ addTransitionType("select-item");
+ router.push("/detail/1");
+ });
+ ```
+
+ Pass an object to map types to CSS classes. Works on `enter`, `exit`, **and** `share`:
+
+ ```jsx
+ <ViewTransition
+ enter={{
+ "nav-forward": "slide-from-right",
+ "nav-back": "slide-from-left",
+ default: "none",
+ }}
+ exit={{
+ "nav-forward": "slide-to-left",
+ "nav-back": "slide-to-right",
+ default: "none",
+ }}
+ share={{
+ "nav-forward": "morph-forward",
+ "nav-back": "morph-back",
+ default: "morph",
+ }}
+ default="none"
+ >
+ <Page />
+ </ViewTransition>
+ ```
+
+ `enter` and `exit` don't have to be symmetric. For example, fade in but slide out directionally:
+
+ ```jsx
+ <ViewTransition
+ enter={{ 'nav-forward': 'fade-in', 'nav-back': 'fade-in', default: 'none' }}
+ exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
+ default="none"
+ >
+ ```
+
+ **TypeScript:** `ViewTransitionClassPerType` requires a `default` key in the object.
+
+ For apps with multiple pages, extract the type-keyed VT into a reusable wrapper:
+
+ ```jsx
+ export function DirectionalTransition({ children }: { children: React.ReactNode }) {
+ return (
+ <ViewTransition
+ enter={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
+ exit={{ 'nav-forward': 'nav-forward', 'nav-back': 'nav-back', default: 'none' }}
+ default="none"
+ >
+ {children}
+ </ViewTransition>
+ );
+ }
+ ```
+
+ ### `router.back()` and Browser Back Button
+
+ `router.back()` and the browser's back/forward buttons do **not** trigger view transitions (`popstate` is synchronous, incompatible with `startViewTransition`). Use `router.push()` with an explicit URL instead.
+
+ ### Types and Suspense
+
+ Types are available during navigation but **not** during subsequent Suspense reveals (separate transitions, no type). Use type maps for page-level enter/exit; use simple string props for Suspense reveals.
+
+ ---
+
+ ## Shared Element Transitions
+
+ Same `name` on two VTs — one unmounting, one mounting — creates a shared element morph:
+
+ ```jsx
+ <ViewTransition name="hero-image">
+ <img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
+ </ViewTransition>
+
+ // On the other view — same name
+ <ViewTransition name="hero-image">
+ <img src="/full.jpg" />
+ </ViewTransition>
+ ```
+
+ - Only one VT with a given `name` can be mounted at a time — use unique names (`photo-${id}`). Watch for reusable components: if a component with a named VT is rendered in both a modal/popover _and_ a page, both mount simultaneously and break the morph. Either make the name conditional (via a prop) or move the named VT out of the shared component into the specific consumer.
+ - `share` takes precedence over `enter`/`exit`. Think through each navigation path: when no matching pair forms (e.g., the target page doesn't have the same name), `enter`/`exit` fires instead. Consider whether the element needs a fallback animation for those paths.
+ - Never use a fade-out exit on pages with shared morphs — use a directional slide instead.
+
+ ---
+
+ ## Common Patterns
+
+ ### Enter/Exit
+
+ ```jsx
+ {
+ show && (
+ <ViewTransition enter="fade-in" exit="fade-out">
+ <Panel />
+ </ViewTransition>
+ );
+ }
+ ```
+
+ ### List Reorder
+
+ ```jsx
+ {
+ items.map((item) => (
+ <ViewTransition key={item.id}>
+ <ItemCard item={item} />
+ </ViewTransition>
+ ));
+ }
+ ```
+
+ Trigger inside `startTransition`. Avoid wrapper `<div>`s between list and VT.
+
+ ### Composing Shared Elements with List Identity
+
+ Shared elements and list identity are independent concerns — don't confuse one for the other. When a list item contains a shared element (e.g., an image that morphs into a detail view), use two nested `<ViewTransition>` boundaries:
+
+ ```jsx
+ {
+ items.map((item) => (
+ <ViewTransition key={item.id}>
+ {" "}
+ {/* list identity */}
+ <Link href={`/items/${item.id}`}>
+ <ViewTransition name={`item-image-${item.id}`} share="morph">
+ {" "}
+ {/* shared element */}
+ <Image src={item.image} />
+ </ViewTransition>
+ <p>{item.name}</p>
+ </Link>
+ </ViewTransition>
+ ));
+ }
+ ```
+
+ The outer VT handles list reorder/enter animations. The inner VT handles the cross-route shared element morph. Missing either layer means that animation silently doesn't happen.
+
+ ### Force Re-Enter with `key`
+
+ ```jsx
+ <ViewTransition key={searchParams.toString()} enter="slide-up" default="none">
+ <ResultsGrid />
+ </ViewTransition>
+ ```
+
+ **Caution:** If wrapping `<Suspense>`, changing `key` remounts the boundary and refetches.
+
+ ### Suspense Fallback to Content
+
+ Simple cross-fade:
+
+ ```jsx
+ <ViewTransition>
+ <Suspense fallback={<Skeleton />}>
+ <Content />
+ </Suspense>
+ </ViewTransition>
+ ```
+
+ Directional reveal:
+
+ ```jsx
+ <Suspense
+ fallback={
+ <ViewTransition exit="slide-down">
+ <Skeleton />
+ </ViewTransition>
+ }
+ >
+ <ViewTransition enter="slide-up" default="none">
+ <Content />
+ </ViewTransition>
+ </Suspense>
+ ```
+
+ For more patterns, see `references/patterns.md`.
+
+ ---
+
+ ## How Multiple VTs Interact
+
+ Every VT matching the trigger fires simultaneously in a single `document.startViewTransition`. VTs in **different** transitions (navigation vs later Suspense resolve) don't compete.
+
+ ### Use `default="none"` Liberally
+
+ Without it, every VT fires the browser cross-fade on **every** transition — Suspense resolves, `useDeferredValue` updates, background revalidations. Always use `default="none"` and explicitly enable only desired triggers.
+
+ ### Two Patterns Coexist
+
+ **Pattern A — Directional slides:** Type-keyed VT on each page, fires during navigation.
+ **Pattern B — Suspense reveals:** Simple string props, fires when data loads (no type).
+
+ They coexist because they fire at different moments. `default="none"` on both prevents cross-interference. Always pair `enter` with `exit`. Place directional VTs in page components, not layouts.
+
+ ### Nested VT Limitation
+
+ When a parent VT exits, nested VTs inside it do **not** fire their own enter/exit — only the outermost VT animates. Per-item staggered animations during page navigation are not possible today. See [react#36135](https://github.com/facebook/react/pull/36135) for an experimental opt-in fix.
+
+ ---
+
+ ## Next.js Integration
+
+ For Next.js setup (`experimental.viewTransition` flag, `transitionTypes` prop on `next/link`, App Router patterns, Server Components), see `references/nextjs.md`.
+
+ ---
+
+ ## Accessibility
+
+ Always add the reduced motion CSS from `references/css-recipes.md` to your global stylesheet.
+
+ ---
+
+ ## Reference Files
+
+ - **`references/implementation.md`** — Step-by-step implementation workflow.
+ - **`references/patterns.md`** — Patterns, animation timing, events API, troubleshooting.
+ - **`references/css-recipes.md`** — Ready-to-use CSS animation recipes.
+ - **`references/nextjs.md`** — Next.js App Router patterns and Server Component details.
+
+ ## Full Compiled Document
+
+ For the complete guide with all reference files expanded: `AGENTS.md`