CSS
    animation

    View Transitions API: Effortless Page Animations Without JavaScript

    One JavaScript call, or none at all for cross-document navigations, replaces hand-rolled FLIP animations built from getBoundingClientRect() and transforms.

    Editor: Paul RadfordJun 2, 20267 min read

    Paul Radford — Full-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

    View Transitions API: Effortless Page Animations Without JavaScript

    The View Transitions API lets browsers animate changes between two DOM states, including full page navigations, by automatically capturing before-and-after screenshots and cross-fading or morphing between them. You write a few lines of CSS and one JavaScript call (or none at all for cross-document navigations), and the browser handles the capture, animation, and cleanup.

    What Problem Does This Actually Solve?

    Before this API existed, animating a route change meant one of two bad options. Either you built a single-page app, tracked old and new DOM nodes yourself, and hand-rolled FLIP animations with getBoundingClientRect() and CSS transforms, or you accepted the jarring instant swap that every multi-page site has shipped since 1995.

    The View Transitions API removes the manual bookkeeping. The browser takes a screenshot of the old state, lets the DOM update, takes a screenshot of the new state, and then runs a CSS animation between the two. You get a smooth crossfade by default, and if you name elements with view-transition-name, the browser produces a morph animation between matched elements instead of a flat fade.

    This matters most for two use cases: same-document SPA transitions (supported in Chromium since Chrome 111, March 2023) and cross-document MPA transitions (supported in Chromium since Chrome 126, mid-2024). Firefox and Safari have been slower to ship this. As of early 2026, Firefox still has it behind experimental flags, and Safari's support is partial. Check caniuse before you commit to it as your only animation strategy.

    Same-Document Transitions: The SPA Case

    If you are updating the DOM with JavaScript, either through a framework or vanilla code, the API is a single method call: document.startViewTransition().

    javascript
    1// Wrap any DOM mutation in startViewTransition to get an automatic animation
    2function navigateTo(newContent) {
    3 // Feature-detect first: older browsers just skip the animation
    4 if (!document.startViewTransition) {
    5 updateContent(newContent);
    6 return;
    7 }
    8
    9 const transition = document.startViewTransition(() => {
    10 // This callback must be synchronous (or return a Promise the API awaits)
    11 updateContent(newContent);
    12 });
    13
    14 // Optional: react to lifecycle events
    15 transition.ready.then(() => {
    16 // Runs after the pseudo-element tree is built, before the animation starts
    17 });
    18
    19 transition.finished.then(() => {
    20 // Runs after the animation completes and old snapshots are discarded
    21 });
    22}
    23
    24function updateContent(newContent) {
    25 document.querySelector('#app').innerHTML = newContent;
    26}

    The default animation is a cross-fade between old and new screenshots of the whole document. To get more interesting transitions, tag specific elements so the browser can track them individually:

    css
    1/* Give the header a stable transition name so it morphs instead of fading */
    2.page-header {
    3 view-transition-name: page-header;
    4}
    5
    6/* Give the hero image its own named transition */
    7.hero-image {
    8 view-transition-name: hero-image;
    9}
    10
    11/* Customize the default animation duration and easing */
    12: :view-transition-old(root),
    13: :view-transition-new(root) {
    14 animation-duration: 300ms;
    15}
    16
    17/* Target a specific named element's animation */
    18: :view-transition-old(hero-image),
    19: :view-transition-new(hero-image) {
    20 animation-duration: 450ms;
    21 animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
    22}

    Each named element gets its own pseudo-element pair (::view-transition-old(name) and ::view-transition-new(name)), so you can choreograph them independently. This is the mechanism behind those shared-element "hero" transitions you see in native mobile apps, now doable in three lines of CSS.

    Can You Really Skip JavaScript Entirely?

    Yes, for cross-document navigations, which is the scenario the title refers to. As of Chrome 126, you can animate a full page load, meaning a real navigation with a server round-trip, using only CSS and an opt-in meta tag or @view-transition rule.

    css
    1/* Add this to both the outgoing and incoming page's stylesheet */
    2@view-transition {
    3 navigation: auto;
    4}
    5
    6/* Optional: customize the crossfade for cross-document navigation */
    7: :view-transition-old(root) {
    8 animation: 200ms ease-out fade-out;
    9}
    10
    11: :view-transition-new(root) {
    12 animation: 200ms ease-in fade-in;
    13}
    14
    15@keyframes fade-out {
    16 to { opacity: 0; }
    17}
    18
    19@keyframes fade-in {
    20 from { opacity: 0; }
    21}

    Both pages involved in the navigation need this rule for the transition to fire; it is not enough to add it to just the destination page. If either page omits it, the browser falls back to a normal, instant navigation with no animation and no error. That silent fallback is convenient for progressive enhancement but can be confusing to debug: if your transition "just doesn't work," check both documents' CSS before assuming a scripting bug.

    This cross-document mode currently only works for same-origin navigations. You cannot animate a transition into a different origin, which rules out cross-site navigation animations for security reasons documented in the WHATWG HTML spec's view transitions section.

    What Breaks, and Where the Gotchas Live

    The biggest practical gotcha is layout thrashing during the callback. The function you pass to startViewTransition() must leave the DOM in its final, stable state by the time it resolves. If you trigger async data fetching inside that callback without awaiting it, the browser will screenshot an incomplete or flickering intermediate state.

    Another one: view-transition-name values must be unique per document at the moment of capture. If two visible elements share the same name during either the "old" or "new" phase, the transition throws and silently aborts (Chrome logs a console warning). This bites people who apply a shared class-based name to list items, for example a .card { view-transition-name: card; } rule applied to a whole <ul> of cards. Every card collides. The fix is to generate unique names dynamically, often with a template literal keyed to an ID: element.style.viewTransitionName = \`card-${id}\`;.

    Reduced motion is also easy to forget. The API does not automatically respect prefers-reduced-motion; you have to write the media query yourself:

    css
    1@media (prefers-reduced-motion: reduce) {
    2 /* Disable all view transition animations for users who asked for less motion */
    3 : :view-transition-group(*),
    4 : :view-transition-old(*),
    5 : :view-transition-new(*) {
    6 animation: none !important;
    7 }
    8}

    Skipping this is an accessibility regression, not just a nice-to-have, and it is the single most common oversight in demo code I have reviewed.

    Finally, be aware that scroll position and focus are not automatically preserved across transitions in every browser. Test explicitly rather than assuming parity with a normal navigation, particularly for cross-document transitions where the "old" page's JavaScript context is already gone by the time the new page paints.

    When Not to Reach for This API

    If your primary audience is on Safari or Firefox stable in 2026, do not build a critical interaction around view transitions, because a meaningful share of users will get the plain, instant fallback. That is fine if you treat the animation as decorative progressive enhancement, and genuinely broken if your layout depends on the animated state to convey information (like an onboarding flow that assumes users saw an element fly from point A to point B).

    If you need frame-by-frame control, physics-based motion, or gesture-driven interruption (drag-to-dismiss, for example), this API is the wrong tool. It gives you a start and end snapshot plus a CSS animation between them; it does not expose an animation timeline you can scrub or interrupt mid-flight the way the Web Animations API or a library like Motion One does. For anything gesture-driven, keep using JavaScript-driven animation and treat view transitions as a separate concern for route-level chrome.

    How to Verify It Works Before You Ship It

    Open DevTools and check the Elements panel during a transition: Chrome exposes the transition pseudo-element tree live, under a synthetic ::view-transition root, while the animation is running, so you can inspect exactly which named elements got captured and confirm there is no naming collision.

    For cross-document transitions, test with the Network tab throttled to a slow connection. Because the "old" snapshot is taken right before unload and the "new" one right after the incoming page's first paint, a slow response can create an unintentionally long freeze between the two snapshots, which reads as a stall rather than an animation. If that happens, either add a lightweight loading-state view-transition-name target, or scope @view-transition { navigation: auto; } to only apply on fast, cached, same-origin routes using server-side logic, rather than globally.

    Cross-browser testing matters here more than usual, given how uneven support still is. Confirm current status directly on caniuse or the MDN View Transitions API reference before launch, since both browser vendors have shipped partial and experimental implementations on different timelines, and the spec itself, tracked at W3C's View Transitions Level 1 draft, continues to evolve. Build your fallback path first, treat the animation as an enhancement layer, and you will ship something that degrades gracefully rather than something that only works in one browser family.

    Related Articles