View Transition Browser Support Matrix
The View Transitions API landed in engines in stages — same-document transitions first, cross-document transitions a year later, and the selector-level refinements (view-transition-class, nested groups) later still — so “does the browser support View Transitions?” is never a yes/no question. This reference breaks support down feature by feature and engine by engine, documents the flags and the per-engine rendering quirks, and ends with a deployment sequence that ships transitions safely to every tier. It sits within the broader Scroll-Driven & View Transition Implementation Patterns section; for the snapshot-and-composite machinery these version numbers gate, see how the View Transition API works under the hood.
Articles in this guide
Feature inventory and syntax reference
The API is not one feature but a family. Each row below shipped independently and must be detected independently:
| Feature | Surface | What it does |
|---|---|---|
document.startViewTransition(cb) |
JavaScript | Snapshots the page, runs cb to mutate the DOM, animates old → new (same document) |
::view-transition, ::view-transition-group(), ::view-transition-old(), ::view-transition-new() |
CSS pseudo-elements | The generated tree you style to customize the animation |
view-transition-name |
CSS property | Tags an element for an independent transition group |
@view-transition { navigation: auto; } |
CSS at-rule | Opts a document into cross-document transitions on same-origin navigation |
view-transition-class |
CSS property | Lets one pseudo-element rule target many named groups |
view-transition-group (property) |
CSS property | Nests transition groups so children clip and transform with their parent snapshot |
startViewTransition({ update, types }) |
JavaScript | Typed transitions; :active-view-transition-type() matches in CSS |
The distinction that matters most for support planning is same-document versus cross-document, because they shipped roughly a year apart in both Chromium and WebKit. The same-document vs cross-document View Transitions guide covers the behavioral differences; the tables below cover only availability.
Same-document support matrix
Same-document transitions — document.startViewTransition() plus the ::view-transition-* pseudo-element tree — have the widest coverage:
| Engine / browser | First stable version | Status | Notes |
|---|---|---|---|
| Chrome | 111 (March 2023) | Shipped | Full pseudo-element tree, transition.finished / .ready / .updateCallbackDone promises |
| Edge | 111 (March 2023) | Shipped | Tracks Chromium; identical behavior |
| Opera | 97 | Shipped | Chromium-based; follows Chrome 111 |
| Samsung Internet | 23 | Shipped | Chromium-based; verify on-device, as GPU snapshot limits differ on low-end hardware |
| Safari | 18 (September 2024) | Shipped | macOS Sequoia / iOS 18; earlier versions have no API at all |
| Firefox | — | In development | Implementation in progress behind a preference in Nightly builds (targeted at the Firefox 144+ line); treat as unsupported in production |
Practical reading of this table: Chromium coverage is deep (three-plus years of stable releases), Safari coverage starts abruptly at 18 with nothing before it, and Firefox must be treated as a no-support engine until its implementation ships and stabilizes. Any architecture that assumes startViewTransition exists will throw a TypeError for a meaningful share of users — which is why the wrapper pattern in detecting View Transition support with @supports and JavaScript is mandatory, not optional.
Cross-document @view-transition support
Cross-document transitions animate full multi-page navigations without JavaScript. Both the outgoing and incoming document must opt in with the at-rule, and the navigation must be same-origin:
| Engine / browser | First stable version | Status | Notes |
|---|---|---|---|
| Chrome | 126 (June 2024) | Shipped | pageswap / pagereveal events for JS customization |
| Edge | 126 (June 2024) | Shipped | Tracks Chromium |
| Safari | 18.2 (December 2024) | Shipped | A narrow gap exists: Safari 18.0–18.1 supports same-document only |
| Firefox | — | Not shipped | Same-document work must land first; treat as unavailable |
/* Must appear in the CSS of BOTH documents involved in the navigation */
@view-transition {
navigation: auto;
}
The at-rule is its own progressive-enhancement mechanism: an engine that does not recognize @view-transition drops the whole block during parsing and performs a normal, instant navigation. There is no error state and no broken layout — which makes cross-document transitions the safest View Transition feature to deploy, despite being the newest widely shipped one.
The Safari 18.0–18.1 window deserves emphasis because it breaks the assumption that same-document and cross-document support travel together. A user on iOS 18.1 gets your SPA page-swap animations but not your multi-page transitions. If your analytics show a significant share of un-updated iOS 18.0/18.1 devices, plan for that split tier explicitly.
view-transition-class and nested groups
Two refinements shipped after the core API and have their own support boundaries.
view-transition-class solves the selector explosion problem. Without it, styling forty card transitions requires forty ::view-transition-group(--card-1) … ::view-transition-group(--card-40) rules; with it, one class-based rule covers them all:
.card {
/* each card still needs a unique name */
view-transition-name: var(--card-name);
/* but they share one class for styling */
view-transition-class: card;
}
/* one rule targets every group carrying the class */
::view-transition-group(*.card) {
animation-duration: 280ms;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}
Support: Chrome/Edge 125+, and Safari added it in the 18.x line alongside its cross-document work — verify against your minimum target Safari version rather than assuming it arrived in 18.0. Critically, @supports (view-transition-name: none) returns true in Chrome 111–124 where view-transition-class does not exist, so class-based rules silently fail to match there. Detect it separately with @supports (view-transition-class: none) if the styling difference matters.
Nested transition groups (the view-transition-group CSS property, distinct from the pseudo-element of the same name) let a child group render inside its parent’s group rather than in the flat top-level tree, so clipping, borders, and 3D transforms of the parent apply to the child snapshot during the animation. This is the newest piece of the family: it is available only in newer Chromium releases (Chrome 139-era and later) and should be treated as an experimental enhancement, not a dependency. The flat-tree behavior is the only portable assumption — a constraint that also shapes the comparison in View Transition API vs the FLIP technique, since hand-rolled FLIP keeps elements in the real DOM tree where ancestor clipping always applies.
Per-engine quirks
Version numbers say whether the API exists; these quirks say how it behaves once it does.
Snapshot rendering differences
- Snapshots are flattened raster/replaced content. In every engine,
::view-transition-old()is a static capture and::view-transition-new()renders as replaced content sized byobject-fit-like rules. Text inside a snapshot does not reflow when the group’s size animates — it scales. Chromium and WebKit apply slightly different snapshot scaling filters, so text that scales during a transition can look crisper in one engine than the other at the same duration. - Ink overflow. Chromium captures ink overflow (box shadows, blur, outline) beyond the border box into the snapshot; WebKit’s capture bounds have differed in edge cases, so a heavy
box-shadowon a morphing element is worth a visual check in Safari before shipping. - Root snapshot and fixed-position elements. The root group captures the full viewport, including
position: fixedelements. A fixed header that should persist visually across the transition must be given its ownview-transition-namein all engines, but the artifacts you get when you forget differ: Chromium tends to show the header crossfading with itself, while Safari builds have shown brief double-rendering. Name it explicitly everywhere. - Snapshot freezing. Video, canvas, and CSS animations inside a captured element freeze in the old snapshot in all engines, but whether the new side renders live during the animation is engine-dependent (Chromium renders the new side live; treat any mid-transition liveness as unspecified).
Iframe handling
- Same-document transitions initiated inside an iframe run within that iframe’s own viewport in Chromium; the parent page is untouched.
- Cross-document transitions do not fire for iframe navigations in any engine —
@view-transitionapplies to top-level, same-origin traversals only. - An iframe that happens to sit inside a captured element is flattened into the snapshot like any other content: its internal scrolling and playback freeze on the old side for the duration of the transition. Cross-origin iframe content is captured as it was rendered, with no additional data exposed — but if the flash of frozen third-party UI is unacceptable, exclude the region from morphing by keeping it inside the root group.
Firefox (in development)
Firefox’s implementation can be enabled in Nightly via the dom.viewTransitions.enabled preference in about:config. Behind the flag, expect a partially complete feature: the core startViewTransition flow works, while newer pieces (types, view-transition-class, cross-document) trail behind. Never gate production code on the flag — its only legitimate use is pre-testing your fallback and enhancement paths against a fourth engine before Firefox ships.
Flags and pre-release channels
| Channel | How to enable | What you get |
|---|---|---|
| Firefox Nightly | about:config → dom.viewTransitions.enabled → true |
In-development same-document implementation |
| Chrome Canary / Dev | chrome://flags → “Experimental Web Platform features” |
Spec features not yet stable (nested groups before their release, newer typed-transition behavior) |
| Safari Technology Preview | Develop menu → Feature Flags | WebKit’s in-progress View Transition refinements |
Flags are for testing your guards, not for users. The four tiers you should actually test against are: full support (current Chrome/Edge), same-document-only (Safari 18.0–18.1), flagged/partial (Firefox Nightly), and none (Firefox stable, older Safari).
@supports guard recipes
CSS-side detection uses property support as a proxy for the whole same-document feature, following the same discipline as the site-wide browser support and progressive enhancement guide:
/* Baseline: navigation works, elements are visible, no transition styling */
/* Enhancement tier 1: same-document View Transitions */
@supports (view-transition-name: none) {
.hero__title {
view-transition-name: hero-title;
}
::view-transition-old(hero-title),
::view-transition-new(hero-title) {
animation-duration: 240ms;
}
}
/* Enhancement tier 2: class-based group styling (newer engines only) */
@supports (view-transition-class: none) {
.card {
view-transition-class: card;
}
::view-transition-group(*.card) {
animation-duration: 280ms;
}
}
/* Reduced motion wins over both tiers */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation-duration: 0.01ms !important;
}
}
Why view-transition-name: none rather than a real name? none is the property’s initial value, so the query tests pure parser support without side effects, and it is valid in every engine that implements the property. An alternative form, @supports selector(::view-transition), tests the pseudo-element directly; it is useful when your enhancement is purely pseudo-element styling, but property-based queries are the more widely used convention and behave identically in practice.
Cross-document transitions need no guard at all — the @view-transition at-rule self-gates, as covered above. What you cannot do is detect the at-rule with @supports, because @supports tests declarations and selectors, not at-rules. If JavaScript needs to know whether cross-document transitions ran, listen for the pagereveal event and check event.viewTransition for a non-null value.
Safe progressive-deployment recipe
The sequence below ships View Transitions to every supporting tier with zero risk to the rest:
- Ship the baseline first. Every route change, DOM swap, and multi-page navigation must work with all View Transition code deleted. If removing the transition breaks navigation, the transition is load-bearing and the architecture is wrong.
- Add same-document CSS inside
@supports (view-transition-name: none). Names, group styling, and custom keyframes all live in the guarded block. Non-supporting engines parse none of it. - Wrap
startViewTransitionin a fallback function. Check'startViewTransition' in document; when absent, invoke the DOM-update callback directly so the mutation still happens instantly. The full wrapper, including the reduced-motion guard and promise-shape compatibility, is built step by step in the detection and fallback article. - Opt in to cross-document transitions with the at-rule only. No JS is required; add
pageswap/pagereveallisteners later if you need per-navigation customization, and feature-test those events too ('onpagereveal' in window). - Layer the reduced-motion override on top. Both the JS wrapper and the CSS pseudo-elements need it, because they are independent code paths.
- Verify each tier separately. Current Chrome (everything), Safari 18.0 (same-document only), Firefox stable (nothing — confirm the instant fallback), Firefox Nightly with the pref (early warning for engine differences).
Deployed in this order, each step is independently revertible and no step can regress a browser that a previous step already served.
Gotchas and failure modes
@supports (view-transition-name: none)is true whereview-transition-classis false. Chrome 111–124 and Safari 18.0 pass the first query and fail the second. Feature-detect at the granularity you consume; one guard per feature family, not one guard for “View Transitions.”- Calling
startViewTransitionunguarded throws.document.startViewTransition(...)is aTypeErrorin Firefox stable and Safari 17. Because the DOM update lives inside the callback, an unguarded call means the navigation itself silently never happens — the worst possible failure mode. The JS guard is not cosmetic. - Duplicate
view-transition-namevalues skip the transition. If two rendered elements share a name at snapshot time, the transition aborts and thereadypromise rejects. Engines agree on the abort but differ in console diagnostics; Chromium names the offending element, Safari is terser. Audit dynamically assigned names (list items, cards) most. - Cross-document transitions require the at-rule on both sides. Declaring
@view-transitionon only the destination page yields a normal navigation with no error anywhere. When a cross-document transition “randomly” fails, check the outgoing page’s CSS first. - UA sniffing ages badly here. Safari’s same-document/cross-document split (18.0 vs 18.2) and Firefox’s in-flight implementation mean any UA-string table you write is stale within months. Feature detection has no such decay.
- The flat pseudo-tree surprises clipped layouts. Portable behavior hoists every named group to the top-level tree, escaping ancestor
overflow: hiddenandborder-radius. Nested groups fix this only in newer Chromium; if the design depends on clipping during the morph, you need the FLIP-style alternative or a design change. - Flagged Firefox is not shipped Firefox. Behavior behind
dom.viewTransitions.enabledcan change before release. File it under “early signal,” never under “supported.”
Performance checklist
- Keep the set of simultaneously named elements small — every
view-transition-nameallocates snapshot texture memory, and mobile GPUs (Samsung Internet’s typical hardware in particular) hit limits far earlier than desktops. - Skip
startViewTransition()entirely underprefers-reduced-motion: reduce; CSS duration overrides alone still pay the snapshot cost. - Prefer transform/opacity keyframes on
::view-transition-group(); snapshot pseudo-elements composite cheaply, but animating theirwidth/heightstill forces per-frame layout of the pseudo-tree. - Await
transition.finishedbefore scroll restoration or focus moves to avoid mid-animation reflows. - Clear dynamically assigned
view-transition-namevalues after each transition so stale names cannot collide on the next one. - Test cross-document transitions on throttled networks: the incoming page must reach a render-blocking-complete state before the animation starts, so slow critical CSS visibly delays the transition rather than the transition hiding the load.
- Profile in each engine tier, not just Chrome — Safari’s snapshot scaling and Chromium’s live-new-side rendering have different costs for the same markup.
Related
- Implementing View Transitions for React Router — applying the wrapper and guard patterns inside a real routing layer
- Cross-Route Element Morphing — shared-element techniques that depend on the
view-transition-namesupport documented here - Fallback Strategies for Legacy Browsers — what the no-support tier should receive instead of a transition
- Implementing prefers-reduced-motion in Scroll-Driven Animations — the motion-preference layer every deployment step above must respect