Smooth Web Animations with Safari's View Transitions API
This article explores how the View Transitions API in the Safari web browser enables seamless, animated transitions between different DOM states and across multi-page navigations. By leveraging WebKit's rendering engine, Safari captures visual snapshots of web pages before and after an update, allowing developers to morph, fade, and slide elements effortlessly using standard CSS and minimal JavaScript. Below, you will learn about the underlying snapshot mechanism, single-page DOM state updates, multi-page navigation transitions, and how to customize these animations for individual elements.
The Core Mechanism: Snapshots and Pseudo-Elements
Historically, animating between two different DOM states or page loads required complex JavaScript frameworks to keep both the old and new elements in the DOM simultaneously. Safari's implementation of the View Transitions API eliminates this complexity by handling the transition at the browser rendering level.
The process works in four distinct steps:
- Capture the Old State: Safari takes a lightweight rasterized snapshot of the current visual state of the page.
- Update the DOM: The browser suppresses visual rendering while the DOM is updated via JavaScript, or while a new HTML document is fetched and parsed.
- Capture the New State: Safari reads the layout and visual representation of the newly updated DOM.
- Animate via Pseudo-Elements: WebKit constructs a temporary overlay tree of CSS pseudo-elements containing the old snapshot and the live new representation. It then smoothly animates between them—defaulting to a cross-fade—before removing the overlay and revealing the interactive new page.
Transitions Between DOM States (Single-Page Applications)
For single-page applications (SPAs) or dynamic in-page updates,
Safari utilizes the document.startViewTransition()
JavaScript method. When you wrap a DOM-modifying function inside this
method, you instruct Safari to automatically orchestrate the snapshot
lifecycle.
document.startViewTransition(() => {
// Modify the DOM here
container.classList.toggle('expanded-view');
});When this function executes, Safari immediately captures the current
screen, runs the callback function to apply the
.expanded-view class, calculates the new layout, and
cross-fades between the two states. If the DOM update requires fetching
data asynchronously, the callback can return a Promise; Safari will hold
the old snapshot on screen until the Promise resolves, preventing layout
shifts or blank loading states.
Transitions Across Multi-Page Navigations
Safari also supports cross-document view transitions, bringing native-like page transitions to traditional Multi-Page Applications (MPAs) without requiring any JavaScript.
To enable transitions between two distinct URLs, both the origin page and the destination page must belong to the same web origin (same scheme, host, and port) and both must include the following CSS rule:
@view-transition {
navigation: auto;
}When a user clicks a standard link, reloads a page, or uses the
browser's back and forward buttons, Safari detects the
@view-transition rule. It freezes the visual display of the
outgoing page as a snapshot, loads and renders the incoming HTML
document in the background, and executes the transition animation once
the new page is ready to be painted.
Morphing Specific Elements
By default, Safari applies a cross-fade to the entire viewport (the
root transition group). However, the API shines when moving
or morphing specific elements—such as expanding a product thumbnail into
a full-screen header image across pages.
To animate an element independently from the rest of the page, assign
it a unique view-transition-name in CSS:
/* On the old state / page */
.thumbnail {
view-transition-name: product-image;
}
/* On the new state / page */
.hero-banner {
view-transition-name: product-image;
}When Safari sees the same view-transition-name in both
the old and new states, it isolates that element into its own snapshot
layer. WebKit automatically calculates the differences in the element's
bounding box (width, height, and screen coordinates) between the two
states and generates a smooth hardware-accelerated animation that
interpolates its position and size from the .thumbnail
layout to the .hero-banner layout.
For a valid transition, a specific view-transition-name
can only be applied to one rendered element on the page at any given
time.
Customizing Animations with CSS
Because Safari renders view transitions using a generated tree of CSS pseudo-elements, developers have full control over the timing, easing, and keyframes of the animations using standard CSS.
For every named transition (including the default root),
Safari generates the following pseudo-element structure:
::view-transition: The root overlay container that sits above the page content.::view-transition-group(name): Controls the size and position interpolation between the old and new states.::view-transition-image-pair(name): A container holding the old and new visual representations, handling blend modes.::view-transition-old(name): The static snapshot of the element before the change.::view-transition-new(name): The live representation of the element after the change.
You can override Safari's default cross-fade by targeting the
old and new pseudo-elements directly with
custom CSS animations:
/* Slow down the position and size morphing */
::view-transition-group(product-image) {
animation-duration: 0.5s;
animation-timing-function: ease-in-out;
}
/* Slide the old page out to the left */
::view-transition-old(root) {
animation: 0.4s ease-in both slide-out;
}
/* Slide the new page in from the right */
::view-transition-new(root) {
animation: 0.4s ease-out both slide-in;
}By offloading the snapshot capturing and geometry interpolation to WebKit's compositor thread, Safari ensures that these animations run smoothly at high frame rates, even when the main thread is busy parsing new HTML or executing JavaScript.