An Introduction to the View Transitions API

    Craig Buckler
    Share

    The new View Transitions API offers an easier way to animate between two DOM states — even between page loads. It’s a progressive enhancement that works today.

    CSS transitions and animations have revolutionized web effects over the past decade, but not everything is easy. Consider a list of elements — such as ten images with titles — which we want to transition into a new list of elements using a cross-fade. The current approach:

    1. retain the old DOM elements
    2. build the new DOM elements, append them to the page, ensuring they’re in an appropriate location
    3. fade out the old set while fading in the new set, then
    4. (optionally) replace the old DOM elements with the new

    It’s not been possible to simply update the DOM — until now! The View Transitions API uses the following process:

    1. The API takes a snapshot of the current page state.
    2. We update the DOM adding or removing elements as necessary.
    3. The API takes a snapshot of the new page state.
    4. The API animates between the two states, using a default fade or whatever CSS animations we define.

    We only need to update the DOM as we’re already doing. A few lines of additional code can progressively enhance the page when the View Transitions API is available to create presentation-like effects.

    View Transition API

    The API is experimental, but recent Chromium-based browsers support in-page, single-document DOM-based effects.

    A viewTransition API for navigations is also available in Chrome 115+ and offers animations between individual page loads — such as on typical WordPress sites. This is even easier to use and requires no JavaScript.

    Mozilla and Apple haven’t revealed their intentions for implementing the API in Firefox and Safari. Any browser without the View Transitions API will continue to work, so it’s safe to add effects today.

    New Old Techniques

    Developers of a certain age may be experiencing déjà vu. Microsoft added element and whole page transitions in Internet Explorer 4.0 (released in 1997) with further updates in IE5.5 (released in 2000). We could add PowerPoint-inspired boxes, circles, wipes, dissolves, blinds, slides, strips, and spirals with a <meta> tag:

    <meta http-equiv="Page-Enter" content="progid:DXImageTransform.Microsoft.Iris(Motion='in', IrisStyle='circle')">
    <meta http-equiv="Page-Exit" content="progid:DXImageTransform.Microsoft.Iris(Motion='out', IrisStyle='circle')">
    

    Strangely, the technique never became widely adopted. It wasn’t a web standard, but the W3C was in its infancy — and developers were happy to use plenty of other IE-specific technologies!

    Why has it taken a quarter of a century for an alternative to appear?!

    Creating In-page Transitions

    View the following CodePen example in Chrome and click the navigation in the header to see a one-second fade between the two states.

    See the Pen
    Transitions API example 1
    by SitePoint (@SitePoint)
    on CodePen.


    The HTML page has two <article> elements with the IDs article1 and article2 for the blocks of content:

    <main><div id="articleroot">
    
      <article id="article1">
    
        <h2>Article 1 content</h2>
    
        <figure>
          <img src="image1.jpg" width="800" height="500" alt="image" />
        </figure>
    
        <p>Lorem ipsum dolor sit amet...</p>
    
      </article>
    
      <article id="article2">
    
        <h2>Article 2 content</h2>
    
        <figure>
          <img src="image2.jpg" width="800" height="500" alt="image" />
        </figure>
    
        <p>Ut pretium ac orci nec dictum...</p>
    
      </article>
    
    </div></main>
    

    A switchArticle() function handles all DOM updates. It shows or hides each article by adding or removing a hidden attribute. On page load, the active article is determined from the page URL’s location.hash or, if that’s not set, the first <article> element:

    // get all articles in page
    const article = document.getElementsByTagName('article');
    
    // show one article on page load
    switchArticle();
    
    // show active article
    function switchArticle(e) {
    
      const hash = e?.target?.hash?.slice(1) || location?.hash?.slice(1);
    
      Array.from(article).forEach((a, i) => {
    
        if (a.id === hash || (!hash && !i)) {
          a.removeAttribute('hidden');
        }
        else {
          a.setAttribute('hidden', '');
        }
    
      });
    
    }
    

    An event handler function monitors all page clicks and calls switchArticle() when the user clicks a link with a #hash:

    // navigation click event
    document.body.addEventListener('click', e => {
    
      if (!e?.target?.hash) return;
      switchArticle(e);
    
    });
    

    We can now update this handler to use View Transitions by passing the switchArticle() function as a callback to document.startViewTransition() (checking the API is available first):

    document.body.addEventListener('click', e => {
    
      if (!e?.target?.hash) return;
    
      if (document.startViewTransition) {
    
        // use View Transition effect
        document.startViewTransition(() => switchArticle(e));
    
      }
      else {
    
        // View Transition not available
        switchArticle(e);
      }
    
    });
    

    document.startViewTransition() takes a snapshot of the initial state, runs switchArticle(), takes a new snapshot of the new state, and creates a default half-second fade between the two.

    The following selectors are available in CSS to target the old and new states:

    ::view-transition-old(root) {
      /* animate out effects */
    }
    
    ::view-transition-new(root) {
      /* animate in effects */
    }
    

    The example above increases the animation duration to one second so the fade effect is more noticeable:

    ::view-transition-old(root),
    ::view-transition-new(root) {
      animation-duration: 1s;
    }
    

    A view-transition-group(root) can apply effects to both old and new states at the same time, although we’re unlikely to apply the same animation in most cases.

    Asynchronous DOM Updates

    The callback passed to document.startViewTransition() can return a promise so asynchronous updates are possible. For example:

    document.startViewTransition(async () => {
    
      const response = await fetch('/some-data');
      const json = await response.json();
      doDOMUpdates(json);
      await sendAnalyticsEvent();
    
    });
    

    This freezes the page until the promise fulfills, so delays could impact the user experience. It’s more efficient to run as much code as possible outside the call to .startViewTransition(). For example:

    const response = await fetch('/some-data');
    const json = await response.json();
    
    document.startViewTransition(() => doDOMUpdates(json));
    
    await sendAnalyticsEvent();
    

    Creating More Sophisticated Transitions

    The following CodePen demo adds a nicer animation using the ::view-transition-old(root) and ::view-transition-new(root) selectors.

    See the Pen
    Transitions API example 2
    by SitePoint (@SitePoint)
    on CodePen.


    The CSS defines transition-out and transition-in animations with fading and rotation:

    ::view-transition-old(root) {
      animation: 1s transition-out 0s ease;
    }
    
    ::view-transition-new(root) {
      animation: 1s transition-in 0s ease;
    }
    
    @keyframes transition-out {
      from {
        opacity: 1;
        translate: 0;
        rotate: 0;
      }
      to {
        opacity: 0;
        translate: -3rem -5rem;
        rotate: -10deg;
      }
    }
    
    @keyframes transition-in {
      from {
        opacity: 0;
        translate: 3rem 5rem;
        rotate: -10deg;
      }
      to {
        opacity: 1;
        translate: 0;
        rotate: 0;
      }
    }
    

    The animations apply to the whole page — including the <header> element, which looks a little strange. We can apply animations (or no animation) to individual elements by setting a view-transition-name:

    header {
      view-transition-name: header;
    }
    

    We can now target that element and apply a different animation:

    ::view-transition-old(header) {
    }
    
    ::view-transition-new(header) {
    }
    

    In this case, we don’t want the header to have any effects, so it’s not necessary to define any animation. The ::view-transition-old(root) and ::view-transition-new(root) selectors now apply to all elements except for the <header>. It remains in-place.

    See the Pen
    Transitions API example 3
    by SitePoint (@SitePoint)
    on CodePen.


    Because we’re defining effects in CSS, we can use developer tool features such as the Animations panel to examine and debug our animations in more detail.

    Using the Web Animations API

    While CSS is enough for most effects, the Web Animations API permits further timing and effect control in JavaScript.

    document.startViewTransition() returns an object that runs a .ready promise which resolves when the transition old and new pseudo-elements are available (note the pseudoElement property in the second .animate() parameter):

    // use the Web Animations API
    const transition = document.startViewTransition( doDOMupdate );
    
    transition.ready.then( () => {
    
      document.documentElement.animate(
        [
          { rotate: '0deg' },
          { rotate: '360deg' },
        ],
        {
          duration: 1000,
          easing: 'ease',
          pseudoElement: '::view-transition-new(root)',
        }
      );
    
    });
    

    Creating Multi-page Navigation Transitions

    We can also use View Transitions as the user navigates between page loads on multi-page applications (MPA) such as typical WordPress sites. It’s known as the viewTransition API for navigations, which we must enable in chrome://flags/ in Chrome 115 (currently the Canary nightly build for developers). The flag is also available in previous releases of the browser, but the API may be missing or unstable.

    The process is easier than in-page transitions because it’s enabled with a single meta tag in the HTML <head>:

    <meta name="view-transition" content="same-origin" />
    

    We can then define ::view-transition-old and ::view-transition-new CSS selectors in an identical way to those shown above. We don’t require any JavaScript unless we want to use the Web Animations API.

    The navigations API may or may not be enabled by default when Chrome 115 final is released. We can use the technique today because browsers that don’t support the API will fall back to standard, non-animated page loads.

    Disabling Animations

    Animations can trigger discomfort for some people with motion disorders. Most operating systems provide a user preference setting to disable effects. We can detect this with the CSS prefers-reduced-motion media query and switch off animations accordingly:

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

    Summary

    The View Transitions API simplifies animations when altering element states in-page and between page loads. Transitions of this type were possible before, but they required a considerable amount of JavaScript — and we had to be careful not to break browser navigation such as the back button.

    The API is new. There’s no guarantee it will remain unchanged, become a W3C standard, or have implementations in Firefox and Safari. However, we can use the API today because it’s a progressive enhancement. Our applications will continue to work in browsers which don’t support the API; they just won’t show animations. There’s a risk certainty the API will change but, even if we have to do some maintenance, our old code shouldn’t break the site and updates are likely to be minimal.

    The downside? The API could lead to an explosion of annoyingly long and wild animations across the Web because site owners consider them to be “on-brand”! Ideally, animations should be fast and subtle to highlight a UI change. Less is often more.

    Further references: