View Transitions skill for Codex
Native page morphs that degrade cleanly. Codex adds native page and state transitions using the View Transitions API, choosing same-document or cross-document mode from the site's architecture. Shared elements get unique names assigned at the right moment, data loads before the snapshot so input never freezes, and every transition is feature-detected with a reduced-motion path, so browsers without support still navigate normally.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- view-transitions Installs to ~/.agents/skills/view-transitions/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install view-transitions The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project view-transitions Installs to .agents/skills/view-transitions/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/view-transitions.zipunzip -o view-transitions.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) view-transitions PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $view-transitions, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Adds View Transitions API animations to single-page apps (document.startViewTransition) and multi-page sites (@view-transition navigation auto), with view-transition-name shared elements, view-transition-class, transition types, framework router integration and reduced-motion fallbacks. Use when a user wants smooth page or route transitions, a thumbnail that morphs into a hero image across pages, cross-document transitions on a static or server-rendered site, or when a view transition flickers, skips or throws InvalidStateError.
Use it when
- Route or page changes should animate without a heavy animation library
- An element should visually carry over from one view to the next
- An existing view transition flickers, skips or blocks input
Use something else when
- Component-level enter, exit or reorder animation inside React: use motion-animations
- Scroll-linked storytelling and pinned sections: use gsap-animations
How to check the result
Scrub the ::view-transition pseudo-element tree in the DevTools Animations panel, navigate rapidly to check for duplicate-name errors, and confirm unsupported browsers navigate with no errors.
The mistake it prevents
Two elements sharing one view-transition-name at capture time, which aborts the whole transition; assign the name only to the clicked item.
Done well means: navigation still works instantly where the API is missing, every shared
view-transition-name is unique at capture time, transitions finish in under ~400 ms, input is
never blocked by a slow update, and reduced-motion users get a cross-fade or nothing.
Support model (late 2026)
| Feature | Chromium | Safari | Firefox |
|---|---|---|---|
Same-document (document.startViewTransition, view-transition-name) | Yes | 18+ | 144+ |
view-transition-class, :active-view-transition | Yes | Yes | 144+ |
Cross-document (@view-transition { navigation: auto }) | 126+ | 18.2+ | Not yet |
Transition types (types option, :active-view-transition-type()) | Yes | Yes | Check before relying on it |
Treat the API as progressive enhancement: feature-detect, and make the non-animated path the correct one. Re-check MDN or caniuse when exact versions matter; this table ages.
Pick the mode
| Site | Mode |
|---|---|
| MPA, server-rendered pages, Astro without client router, static site | Cross-document: CSS opt-in on both pages |
| SPA with a client router (React Router, Vue Router, SvelteKit, TanStack) | Same-document: wrap the DOM update in startViewTransition |
| Next.js App Router | Same-document via the router’s experimental support, or a small wrapper around router.push; check the installed version’s docs |
Astro with <ClientRouter /> | Built in; use transition:name / transition:animate |
| One component changing state (list filter, tab panel) | Same-document on that update |
Workflow
- [ ] 1. Read AGENTS.md; identify router, framework version, and browser support policy
- [ ] 2. Choose mode (table above); add feature detection
- [ ] 3. Get the default cross-fade working end to end first
- [ ] 4. Add named shared elements one at a time
- [ ] 5. Customize animations with ::view-transition-* pseudo-elements or classes
- [ ] 6. Add reduced-motion and slow-update handling
- [ ] 7. Verify in DevTools Animations panel and in a browser without support
Same-document (SPA)
export function withTransition(update: () => void | Promise<void>, types: string[] = []) {
if (!document.startViewTransition ||
matchMedia("(prefers-reduced-motion: reduce)").matches) {
return void update();
}
const vt = document.startViewTransition({ update, types });
vt.finished.catch(() => {}); // skipped transitions reject; not an app error
return vt;
}
- The update callback must change the DOM synchronously or return a promise that resolves
once it has. With React, wrap the state change in
flushSyncinside the callback so the new DOM exists when the browser captures it. - While the update runs, the page is frozen on the old snapshot and does not accept input.
Fetch data before calling
startViewTransition, then do only the DOM swap inside. - Older Safari versions accept only the function form, not
{ update, types }; if supporting them, feature-detect types or pass the function.
React Router (v6.4+ data routers and v7) accepts viewTransition on <Link>/<NavLink>
and navigate(to, { viewTransition: true }), and exposes useViewTransitionState(to) to set
names only on the item being navigated. Prefer that over hand-wrapping.
Cross-document (MPA)
Both the old and new page must opt in, and they must be same-origin.
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) {
@view-transition { navigation: none; }
}
- The new page renders when it is ready to paint; large render-blocking resources delay the
transition. Keep the critical CSS small.
<link rel="expect" href="#hero" blocking="render">can hold first render until a specific element is parsed; use sparingly. pageswapandpagerevealevents let you set names or types per navigation (for example, only name the clicked card): readevent.viewTransitionandnavigation.activationthere.- Reloads and cross-origin navigations never animate.
Shared elements
.product-card img { view-transition-name: var(--vt-name, none); }
.product-hero img { view-transition-name: product-image; }
::view-transition-group(product-image) {
animation-duration: 320ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
Set the name on the clicked card only, just before the transition
(card.style.setProperty("--vt-name", "product-image")), or in pageswap for MPAs.
Rules:
- A name must be unique in the document at capture time. Two elements with the same name
abort the transition (
InvalidStateError, or a console warning and skip). - For many elements that need their own group (list reorder), use
view-transition-name: match-elementwhere supported, or generate names from IDs (view-transition-name: item-42), and share styling throughview-transition-class: card. - Elements are captured as images; text inside a morphing element scales as a bitmap. Give headings their own name so they cross-fade instead of stretching, or keep aspect ratios equal.
- Names must be valid CSS idents: no leading digit, no spaces; escape or prefix generated IDs.
Customizing the animation
::view-transition-old(root) { animation: 180ms ease-in both fade-out; }
::view-transition-new(root) { animation: 260ms ease-out both slide-in; }
@keyframes fade-out { to { opacity: 0; } }
@keyframes slide-in { from { opacity: 0; translate: 24px 0; } }
/* Direction-aware with types */
html:active-view-transition-type(back) ::view-transition-new(root) {
animation-name: slide-in-from-left;
}
Keep the header and other persistent chrome stable by naming it (view-transition-name: site-header)
so it is not part of the root cross-fade.
Gotchas
startViewTransitionwhile another is running skips the first; rapid clicks are fine butfinishedrejects. Always catch it.- Elements with
view-transition-namebecome stacking contexts and isolated groups during the transition;position: fixedoverlays may appear above or below unexpectedly. - Scroll position changes during an SPA transition; restore scroll inside the update callback, not after, or the snapshot jumps.
- Focus is not moved for you. After an SPA route change, move focus to the new page’s
h1or main region and updatedocument.title. - Long transitions block input; keep total time under ~400 ms.
Verification
- DevTools Animations panel: pause and scrub the
::view-transitiontree; check each named group pairs old with new. - Console shows no duplicate-name errors while navigating rapidly.
- Firefox (MPA mode) and any unsupported browser: navigation works with no errors.
- Reduced motion emulated: no movement, navigation unchanged.
- Keyboard navigation: focus lands on the new content after each route change.
- Throttle the CPU 4x; transition still completes and the page stays responsive.
Output
Report the mode chosen and why, named elements and where names are assigned, custom animations and durations, types used, and the fallback behavior per browser.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "View Transitions"
short_description: "Native page morphs that degrade cleanly"
default_prompt: "$view-transitions Add smooth page transitions to our multi-page site with no JavaScript framework" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.