Home CSS

CSS View Transitions

View Transitions snapshot the page before and after a state change, then animate between the two snapshots. The single API turns 'navigation requires a SPA framework' into 'add five lines of CSS'.

What the API does

The View Transitions API takes a snapshot of the rendered page, applies a DOM update, takes a second snapshot, and runs a CSS animation between the two. Default behaviour: a 0.25-second cross-fade of the whole page.

Two flavours exist:

  • Same-document transitionsdocument.startViewTransition(callback). The callback mutates the DOM; the engine handles the before/after snapshots and animates.
  • Cross-document transitions — opt-in via the @view-transition rule plus the view-transition-name and view-transition-class properties. Activated automatically on same-origin navigations when both pages opt in.
@view-transition {
  navigation: auto;
}

That single rule, on both pages of a navigation, gives a cross-fade between full-page renders. Cross-document support shipped in Chromium 126 (June 2024) and is in development in WebKit and Gecko as of early 2025.

Anatomy of a transition

When startViewTransition() is called, the engine creates a parallel pseudo-element tree:

::view-transition
└── ::view-transition-group(name)
    └── ::view-transition-image-pair(name)
        ├── ::view-transition-old(name)
        └── ::view-transition-new(name)

For each named element (view-transition-name: foo), the engine takes the old visual rect and the new visual rect, places them in the pair, and animates between them. Default animation: position, size, opacity for the group; opacity for the old/new pair.

To customise:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.25, 0, 0.3, 1);
}

::view-transition-old(card-hero) {
  animation-name: slide-out-left;
}
::view-transition-new(card-hero) {
  animation-name: slide-in-right;
}

Naming groups

view-transition-name: <ident> declares which DOM element should get its own transition group. Anything not named ends up in the implicit root group.

.card.featured {
  view-transition-name: featured-card;
}
.card.featured img {
  view-transition-name: featured-image;
}

Names must be unique on a page during the snapshot. Two visible elements sharing a name produces an error and aborts the transition.

Cross-document choreography

For a same-origin navigation:

/* on every page */
@view-transition { navigation: auto; }

/* on detail page */
.hero { view-transition-name: hero; }

/* on list page, the corresponding card */
.card[data-id="42"] .image { view-transition-name: hero; }

When the user clicks the card, the engine matches hero between old and new pages and slides/scales the image into place. About 60% of the visual polish of a SPA-driven detail-view animation comes from this single name match.

Accessibility

The API respects prefers-reduced-motion: reduce only if the author writes the rule. Default animations run regardless. Author opt-in:

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

About 30% of users on macOS Safari have prefers-reduced-motion set per Apple’s 2023 accessibility usage report; the equivalent is rare on Chromium-on-Windows but common on Chromium-on-ChromeOS.

Common pitfalls

  • Two visible elements with the same name. Aborts the transition. Either uniquify per route or set view-transition-name: none on duplicates.
  • DOM-mutation callback throws. The transition aborts; old and new states are blurred together. Wrap the callback in try/catch and abort cleanly.
  • Heavy synchronous DOM work in the callback. The browser blocks rendering until the callback resolves; jank up to the duration of the work. Pre-warm caches; defer non-critical work.
  • Cross-document on a slow page. The new page must fully load before the transition runs; on a 3-second page the transition feels like a freeze. Pair with the speculationrules prefetch hint to warm the new page early.

Cross-engine support

Same-document transitions reached interop in 2024 across Chromium (Blink) 111+, Safari 18 (September 2024), and Firefox in development. Cross-document transitions are Chromium 126+ only as of mid-2024; WebKit and Gecko targets in 2025.

caniuse for view-transition reports about 78% global support for same-document and 60% for cross-document at end of 2024.

Further reading