View transitions
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 — but sticky headers, hero images, and search-param-only navigation each have gotchas worth documenting up front.
How it works
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)— the box that contains the old + new snapshots for a named element.::view-transition-image-pair(name)— the cross-fade pair.::view-transition-old(name)and::view-transition-new(name)— 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.
Layering sticky headers above content
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 more than 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.
Naming elements
When you name an element with view-transition-name, it gets its own animation group and morphs from its old position + size to its new position + size. Unnamed elements cross-fade together under the root group.
Rule of thumb: when in doubt, name it. Unused names are free — they don't run an animation unless the element actually moves. Naming the hero image, the title, and the header separately is much cheaper than debugging a root-group cross-fade that clobbers half your UI.
Common named groups
site-header— sticky top navcollection-hero— above-the-fold hero imagecollection-title— the title that persists across routescollection-header— the elevated surface that wraps the hero on per-collection routes
Recommended keyframes
Default 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 — 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.
Triggering on search-param-only navigation
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:
The startTransition wrapper keeps React from thrashing state updates during the browser's snapshot capture. Without it you can get flickers when the new page renders faster than the old page's snapshot.
Guidelines
Always feature-detect.
document.startViewTransitionis still gaining adoption — wrap every call in atypeofcheck and fall back to the standard navigation.Reserve view transitions for spatial continuity. Use them when the user would benefit from seeing an element move (hero → detail page, list → filtered list). Skip them for navigation that isn't spatially related.
Stay under 400ms. Longer transitions feel sluggish, especially on search-param navigation where the user expects instant filter feedback.
Test on mobile Safari. iOS has stricter rules about when
startViewTransitioncan run — if you trigger it from an async callback, the captured snapshot may be stale.Don't animate intent. Changing
--accent-hueor--accent-chromaduring a transition produces noticeably ugly interpolation — set the new accent before the transition starts, or after it completes.