ReferenceMotion tokens
The CSS View Transitions API lets you morph, slide, and fade content across navigation boundaries without touching a JavaScript animation library. Roadie components compose cleanly with it. Sticky headers, hero images, and search-param-only navigation each have gotchas, covered below.
When the browser sees a new document during a view transition, it captures a snapshot of the old DOM and a snapshot of the new DOM, then interpolates between them. Each snapshot lives inside a pseudo-element tree:
::view-transition-group(name) is the box that holds the old and new
snapshots for a named element.::view-transition-image-pair(name) is the cross-fade pair.::view-transition-old(name) and ::view-transition-new(name) are the two
snapshots that the browser animates between.Anything without an explicit view-transition-name lives under the implicit
root group. The root group paints first, which is why sticky headers
sometimes end up underneath a hero image during a transition. The hero gets
its own group, and the root group has no z-index.
Give the sticky header its own named group and set a z-index on its
::view-transition-group. Without this, content sliding in from below the
fold will visually pass through or over the header mid-transition.
Pick any integer above the default 0. 100 is enough for most apps. Do the same for floating buttons, bottom sheets, and any other UI that should stay fixed while the page underneath animates.
When you name an element with view-transition-name, it gets its own
animation group and morphs from its old position and size to its new position
and size. Unnamed elements cross-fade together under the root group.
Name only the elements that need their own motion or layering. Names aren't free. The browser captures every rendered element with a name as its own group and runs that group's default animation, even when the element doesn't move. Each name must also be unique on the page, or the browser skips the transition. Naming the hero image, the title, and the header separately is still cheaper than debugging a root-group cross-fade that clobbers half your UI.
Common named groups:
site-header for the sticky top navcollection-hero for the above-the-fold hero imagecollection-title for the title that persists across routescollection-header for the elevated surface that wraps the hero on
per-collection routesDefault view transitions cross-fade for 250ms. Override with a named
@keyframes rule and attach it to either the old or new snapshot.
Keep durations short. Stay under 400ms for navigation-level transitions.
Respect motion tokens and prefers-reduced-motion.
Wrap your ::view-transition-* rules in
@media (prefers-reduced-motion: no-preference) so motion-sensitive users get
an instant swap.
Next.js App Router doesn't trigger a hard navigation when only a search
parameter changes, so browser-native view transitions don't fire
automatically. Wrap the update in document.startViewTransition inside your
navigation handler.
startViewTransition waits for the promise its callback returns before it
captures the new snapshot. router.push returns before Next renders the new
search params, so the callback returns a promise that resolves once they have
rendered. Without it, the browser captures the old page twice and nothing
animates. If href is already the current URL, push it without a transition,
since the params never change and the promise never resolves.
Inside a Navigator shell, Navigator animates
pane-to-pane route changes itself. Don't wrap a router.push that changes the
active pane in document.startViewTransition. Keep this pattern for
query-string updates within a pane's content, or for pages outside a
<Navigator>.
document.startViewTransition is still gaining
adoption. Wrap every call in a typeof check and fall back to the standard
navigation.startViewTransition can run. If you trigger it from an async callback, the
captured snapshot may be stale.--accent-hue or --accent-chroma during a
transition produces noticeably ugly interpolation. Set the new accent
before the transition starts, or after it completes.