Preface

In single-page applications, changing routes usually results in the old page disappearing instantly and the new one appearing instantly. When clicking from a list to a detail page, even though the thumbnail and header image are the same, there’s no visual continuity between them. In the past, adding these transitions often required importing an animation library, manually managing mount/unmount, measuring positions, and synchronizing timings.

Later, browsers introduced the View Transitions API, which uses document.startViewTransition to take snapshots of two UI states and interpolate between them. React has integrated this into its core: use the ViewTransition component to declare animation boundaries, and the framework will call the native API during Transition or Suspense. This API is still in React’s Canary/Experimental channel, and it’s easy to make mistakes with trigger conditions, placement, default="none", and the name of shared elements; usually these mistakes won’t throw an error, just no animation will play.

Vercel has packaged these rules into an Agent Skill: vercel-react-view-transitions. After installing it in tools like Cursor, Claude Code, or Codex that support SKILL.md, the Agent can add transitions to existing applications following a fixed workflow, instead of piecing together CSS temporarily. Vercel’s official Next.js View Transitions guide also links to this same Skill.

What It Is

vercel-react-view-transitions is hosted in vercel-labs/agent-skills, under the directory skills/react-view-transitions/. The name field in SKILL.md is vercel-react-view-transitions, the author is listed as vercel, metadata version is 1.0.0, and the license is MIT. It follows the Agent Skills universal format, so it can be used by tools that support this format such as Cursor, Claude Code, and Codex CLI.

It is not an npm animation library, nor does it play animations for you at runtime. It provides the Agent with implementation instructions: when to add transitions, how to place the ViewTransition component, which CSS to use, and how to integrate it with next/link in Next.js App Router.

One-sentence overview: Use the browser’s native View Transitions API to create spatial page and component transitions in React, without introducing third-party animation libraries.

The directory structure is as follows:

react-view-transitions/
├── SKILL.md
├── AGENTS.md
└── references/
    ├── implementation.md
    ├── patterns.md
    ├── nextjs.md
    └── css-recipes.md

SKILL.md is the core documentation that is always loaded; read the files under references/ for detailed information. AGENTS.md is the full documentation compiled from the reference files.

Core Capabilities

The principle of this Skill is: every ViewTransition should clearly convey the spatial relationship or continuity it represents. If you can’t articulate that, don’t add it.

Implement by applying all applicable patterns in the following priority order, they are not mutually exclusive:

Priority Pattern Conveyed Meaning
1 Shared Element (name) The same object navigating to a deeper level
2 Suspense Reveal Data loading completed
3 List Identity (per-item key) These are still the same items, just their arrangement has changed
4 State Enter/Exit (enter / exit) Something has appeared or disappeared
5 Route Switch (page-level) Navigated to a new location

There are also corresponding animation styles. Hierarchical navigation (list to detail) uses typed nav-forward / nav-back; horizontal tab switching uses fade-in/fade-out or default="none", do not use directional swipes as they will imply non-existent front/back levels; Suspense reveal uses enter / exit strings; background refreshes use default="none" to stay unobtrusive.

Based on these patterns, the covered capabilities can be divided into several sections:

  1. ViewTransition component. Import it from react and wrap the tree that needs animation. React will automatically assign the view-transition-name and call document.startViewTransition in the background. Both the React documentation and this Skill state: do not call startViewTransition yourself — React will interrupt any other ongoing transitions on the page.

  2. Trigger timing. Only startTransition, useDeferredValue, or Suspense will activate the transition. Ordinary setState will not play animations. Trigger types include enter (first insertion during this Transition), exit (first removal), update (DOM changes within the boundary, or its size/position changes due to adjacent siblings), and share (a boundary with the same name unmounts and another mounts). share takes precedence over enter / exit.

  3. addTransitionType. Add type tags during the same Transition, allowing different boundaries to select different CSS based on context. You can call it multiple times. Starting from Next.js 16.2.0, next/link and useRouter().push() / replace() provide transitionTypes, eliminating the need to manually write onNavigate + startTransition + addTransitionType.

  4. View Transition Class and CSS pseudo-elements. enter / exit / update / share / default can take "auto", "none", a custom class name, or an object mapped by type. The corresponding pseudo-elements are ::view-transition-old, ::view-transition-new, ::view-transition-group, and ::view-transition-image-pair. Ready-made recipes are available in references/css-recipes.md, and the Skill requires copying these into your global stylesheet first, instead of writing timing logic from scratch.

  5. Next.js App Router integration. Including directional transitions at the page level (do not place in layouts), loading.tsx as implicit Suspense, shared elements across routes, and cross-fade for the same dynamic segment using key + name + share. ViewTransition and Link with transitionTypes can be written in Server Components; router.push(..., { transitionTypes }), addTransitionType, and startTransition require Client Components.

Installation and Activation

Three official sources mention the same installation command: the Skill’s built-in README, the skills.sh page, and Next.js’s View Transitions guide.

Install only this Skill:

npx skills add vercel-labs/agent-skills --skill vercel-react-view-transitions

You can also use the full repository address:

npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-view-transitions

Install the entire skill set:

npx skills add vercel-labs/agent-skills

This is Vercel’s skills CLI. By default, it installs to the current project, add -g to install to the user directory. You can specify the tool with -a cursor, -a claude-code, or -a codex. The CLI usually places the content in .agents/skills/ and creates symlinks based on the detected Agent. Each tool will also read the following directories:

Official Cursor documentation lists the loading locations:
- Project-level: .agents/skills/, .cursor/skills/
- User-level: ~/.agents/skills/, ~/.cursor/skills/
- Compatible: .claude/skills/, .codex/skills/ and their corresponding home directory paths

Official Claude Code documentation:
- Project-level: .claude/skills/<name>/SKILL.md
- User-level: ~/.claude/skills/<name>/SKILL.md

Common Codex CLI locations:
- Project-level: .codex/skills/
- User-level: ~/.codex/skills/

For manual installation, copy the entire react-view-transitions directory to the corresponding path, ensuring SKILL.md is at the root of the skill. In Cursor, you can also open the sidebar Customize → Skills to check if it has been detected; use /vercel-react-view-transitions to call it explicitly if needed.

The installed content is the instruction manual for the Agent. Whether your application can play animations depends on your React channel and browser support.

  1. Next.js App Router. Starting from Next.js 16, React Canary is built-in, do not run npm install react@canary separately. The current Next.js documentation (guide version 16.3.1, updated 2026-08-07) states: View Transitions work in App Router without additional configuration. The Skill’s references/nextjs.md still recommends adding:
// next.config.js
const nextConfig = {
  experimental: { viewTransition: true },
};
module.exports = nextConfig;

And notes: this flag was historically used to switch React to the experimental channel (required before ViewTransition entered Canary); it no longer serves this purpose now. useSwipeTransition, parentEnter / parentExit are still in the experimental channel, selected by other flags like gestureTransition. If you are using Next.js 15, the 15 documentation still marks this feature as experimental, requiring you to enable the above flag, and the import name at that time was unstable_ViewTransition. When there are conflicting documents, refer to the official guide corresponding to your current Next.js version.

  1. React projects without Next.js. Both the Skill and React documentation state that ViewTransition is not available in stable React, and you need to install react@canary and react-dom@canary.

  2. Browser. React uses the v2 object form of View Transitions, transition types, and view-transition-class. The range supported by the Skill is Chromium 125+, Firefox 144+, Safari 18.2+. The Next.js guide adds: some animations may behave differently on Safari; unsupported browsers will still function normally, just without transitions.

Typical Usage

After installation, the prompt given in the Next.js official guide is:

Add view transitions to this app using the vercel-react-view-transitions skill.

You can also specify a specific transition, such as thumbnail morphing to header image, forward/backward route swipes, or content cross-fade within the same route. The Skill requires the Agent to first perform a code audit according to references/implementation.md, do not skip this step, then copy the CSS recipes, isolate persistent elements, add directional transitions, Suspense reveals, and shared elements.

The following code snippets are from the original Skill and the Next.js guide, and can be directly compared to the code in the repository.

Shared element: ViewTransition on two views uses the same name. During one Transition, one unmounts and the other mounts, and the browser will morph between their positions and sizes.

import { ViewTransition } from 'react';

<ViewTransition name="hero-image">
  <img src="/thumb.jpg" onClick={() => startTransition(() => onSelect())} />
</ViewTransition>

<ViewTransition name="hero-image">
  <img src="/full.jpg" />
</ViewTransition>

A more common pattern in Next.js is pairing list thumbnails and detail header images, and marking the navigation forward with transitionTypes:

<Link href={`/products/${product.id}`} transitionTypes={['nav-forward']}>
  <ViewTransition name={`product-${product.id}`}>
    <Image src={product.image} alt={product.name} width={400} height={300} />
  </ViewTransition>
</Link>

<ViewTransition name={`product-${product.id}`}>
  <Image src={product.image} alt={product.name} width={800} height={600} />
</ViewTransition>

The name must be globally unique, such as photo-${id}. Only one instance of the same name can be mounted at a time; if a reusable component is rendered simultaneously in a modal and a page, the morph animation will fail. When list items need both reordering animations and cross-route morphing, the Skill requires nesting two layers: the outer layer uses key to manage list identity, and the inner layer uses name to manage shared elements.

Directional navigation: Map the enter/exit types on page components (not layouts). Layouts remain mounted between routes, so enter / exit will not trigger when switching pages.

import { ViewTransition } from 'react';

<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"
>
  <Page />
</ViewTransition>

Without next/link’s transitionTypes, use addTransitionType:

import { startTransition, addTransitionType } from 'react';

startTransition(() => {
  addTransitionType('nav-forward');
  addTransitionType('select-item');
  router.push('/detail/1');
});

Starting from Next.js 16.2.0, button navigation can be written as:

'use client';
import { useRouter } from 'next/navigation';

function DetailButton({ href }: { href: string }) {
  const router = useRouter();
  return (
    <button onClick={() => router.push(href, { transitionTypes: ['nav-forward'] })}>
      Open
    </button>
  );
}

transitionTypes only works in App Router; it will be silently ignored in Pages Router, so the same link component can be used in both routers.

Suspense reveal should use string props, not type mappings. Subsequent resolution of Suspense is another separate Transition, which will not carry the navigation type from before.

<Suspense
  fallback={
    <ViewTransition exit="slide-down">
      <Skeleton />
    </ViewTransition>
  }
>
  <ViewTransition enter="slide-up" default="none">
    <AsyncContent />
  </ViewTransition>
</Suspense>

List reordering: Wrap each item in a ViewTransition with a key, and wrap state updates in startTransition. Do not add an extra wrapper ViewTransition between the list and the VT that would intercept update events.

{items.map(item => (
  <ViewTransition key={item.id}>
    <ItemCard item={item} />
  </ViewTransition>
))}

There is a placement rule for enter/exit transitions: ViewTransition must appear before any DOM nodes, wrapping them in a div will disable enter / exit.

<ViewTransition enter="auto" exit="auto">
  <div>Content</div>
</ViewTransition>

Vercel provides a working reference implementation: Demo and Source Code. Refer to globals.css in the demo repository for the complete CSS.

Applicable Scenarios and Notes

This Skill is suitable for: Next.js App Router projects or projects already using React Canary, where you need to implement shared elements from list to detail, forward/backward route transitions, Suspense skeleton to content, list filter reordering, and do not want to use third-party animation libraries.

It is not suitable or requires downgrading for: stable React (not using Next.js and not installed Canary); Pages Router (transitionTypes is invalid); target browsers older than Chromium 125 / Firefox 144 / Safari 18.2. The application will still work when unsupported, just without transitions.

The following limitations come from the Skill and React/Next.js documentation, and mistakes usually result in silent animation failures.

  1. default="none" should be used intentionally. A bare <ViewTransition> will trigger the browser’s default cross-fade on every navigation, every Suspense resolution, and every background refresh. Named shared elements and typed page VTs should add default="none" and explicitly enable the required triggers. However, default="none" will also disable update and undeclared share: if list items and squeezed siblings need position shift animations, keep them as bare VTs or set update="auto"; shared elements must explicitly set share when default="none".

  2. router.back() and browser forward/back buttons do not carry transition types, directional swipes will fall back to the default in the object (usually "none") and will not play; shared element morphing that is not configured by type may still work. To get a complete backward animation, use router.push() with a clear URL and type.

  3. Shared elements require both the old and new views to be rendered during the same Transition. If the target page first suspends to a fallback, the pairing will fail, and when the data arrives, it will trigger another reveal without the nav-forward type. Dynamic routes prefetch only the shell by default; to get complete content, set prefetch={true} and cache the data required for shared elements. Development mode does not prefetch automatically, so directional transitions should be verified in a production build with an empty client cache.

  4. Nested VTs: When the parent mounts/unmounts as a whole, the internal VTs will not trigger their own enter / exit, only the outermost one will animate. Staggered animations for individual items during page navigation are not currently possible. parentEnter / parentExit are still in the experimental channel.

  5. Do not add a fade-out exit to pages with shared morphing, as it will conflict with the morph animation; use directional swipes instead. Do not use bare viewTransitionName styles to “trigger” animations — that only isolates persistent elements (page headers, sidebars)