Skip to content

Chapter 28: Animation, Motion, and the View Transitions API

For most of the web’s history, animation was something you reached for a library to do.

Flash, in its long heyday (Chapter 7), was a frame-by-frame animation tool with the rest of the runtime built around it. When Flash died, the animation work didn’t disappear; it moved into JavaScript libraries. jQuery’s .animate() (Chapter 9) was the early answer. Later, GSAP — the GreenSock Animation Platform, by Jack Doyle, originally released in 2008 as a Flash library and then ported to JavaScript around 2012 — became the gold standard for serious web animation. Framer Motion (originally Pose, then Framer, now a Vercel-aligned project) became the React-native standard. Motion One (Matt Perry, the same author as Framer Motion) emerged as a smaller, framework-agnostic alternative. Each of these libraries earned its place by providing animation capabilities the platform didn’t have.

The platform has, quietly, caught up.

This chapter is about what the platform now provides for animation, what the libraries still legitimately do, and why the View Transitions API in particular closes the most consequential remaining gap — the one that made server-rendered navigation feel jankier than SPA navigation, which was the strongest historical argument for client-side routing.

CSS Animations and Transitions: The Foundation

Section titled “CSS Animations and Transitions: The Foundation”

The platform’s foundational animation capabilities are CSS transitions and CSS animations, both shipped widely by the early 2010s.

A transition declares that property changes should be animated:

button {
background: blue;
transition: background 200ms ease;
}
button:hover {
background: darkblue;
}

When the button’s background changes (because it entered or left a hovered state), the browser animates the value over 200 milliseconds. The animation is declared once; the platform handles every transition automatically as the relevant state changes.

A CSS animation is more general. It can define a keyframe sequence that plays continuously, on element entry, on a trigger, or any time:

@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.notification {
animation: pulse 2s ease-in-out infinite;
}

The browser runs the keyframes continuously. The animation has its own timing, easing, iteration, fill mode, and play state — all declarative, all evaluated by the browser’s animation engine.

For most product animation needs — hover transitions, focus states, button presses, micro-interactions, loading indicators, page-element entries — CSS animations and transitions are sufficient. The platform handles the work. The application code declares the intent and lets the runtime execute it.

The historical complaint about CSS animations was that they were limited. You couldn’t easily chain them. You couldn’t easily pause and resume from JavaScript. You couldn’t easily reverse them. You couldn’t easily animate to a dynamic value. The Web Animations API addressed each of these.

The Web Animations API (WAAPI), reaching general support around 2019, exposes CSS animation capabilities to JavaScript:

element.animate(
[
{ transform: 'scale(1)' },
{ transform: 'scale(1.1)' },
{ transform: 'scale(1)' }
],
{
duration: 300,
easing: 'ease-out',
iterations: 1
}
)

The animate() method returns an Animation object with imperative controls — play(), pause(), reverse(), cancel(), finish(). The animation can be observed for completion (animation.finished is a promise). Multiple animations can be coordinated, sequenced, and composed.

WAAPI fills the imperative-escape-hatch role that CSS animations don’t quite cover. When the application needs to animate to a value computed at runtime, or chain animations based on completion of previous ones, or coordinate timing across multiple elements, WAAPI provides the primitives without requiring a third-party library.

For most product code, WAAPI plus CSS transitions is enough for the animation needs that show up. The complaints about not enough animation in the platform have been answered.

The View Transitions API: The 2023 Inflection

Section titled “The View Transitions API: The 2023 Inflection”

The most consequential recent animation addition is the View Transitions API.

The work was led by Jake Archibald and Bramus Van Damme on Chrome’s team, with input from the rest of the web platform community. The same-document version shipped in Chrome in 2023. The cross-document version followed in 2024, with cross-browser support reaching widespread coverage through 2024 and into 2025.

The capability the API provides is animated transitions between two states of the same document, or between two different documents:

document.startViewTransition(() => {
// synchronous DOM updates: change which view is shown,
// update the data, swap classes, whatever
})

The browser:

  1. Captures a visual snapshot of the current state of the document.
  2. Runs the callback, which makes synchronous DOM changes.
  3. Captures a visual snapshot of the new state.
  4. Animates between the two snapshots using CSS-driven transition rules.

The default animation is a cross-fade between the snapshots. The application can customize the animation per-element by tagging elements with view-transition-name:

.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 500ms;
}

An element with a view-transition-name is matched across the two snapshots. The browser animates it from its old position and appearance to its new position and appearance, treating the element as a continuous unit. If the hero image moves from a small thumbnail to a full-width banner, the transition is a smooth size-and-position interpolation, not a cross-fade. The element appears to be the same physical object moving through the layout.

This is, for many years of work on web animation, the answer the field has been waiting for. Smooth state transitions, declared in CSS, executed by the browser, integrated with the platform’s full rendering pipeline. The previous generation of solutions (Framer Motion’s layoutId, FLIP-pattern libraries like Flipper.js) had been recreating this capability in JavaScript. The platform’s version is faster, more reliable, and free.

The same-document version of View Transitions is significant. The cross-document version is the chapter’s central architectural point.

@view-transition {
navigation: auto;
}

With this single CSS declaration, the browser automatically applies a view transition to every same-origin navigation. The user clicks a link. The browser captures the current page. The new page is fetched and rendered. The new page is captured. The browser animates between the two — using the same view-transition-name-based element matching the same-document API uses.

Elements tagged with view-transition-name are matched across pages. A logo at the top of the page that exists on both old and new pages animates from its old position to its new position. A hero image on a product page that also appears as a thumbnail on the previous page animates from the thumbnail’s position to the hero’s position. The continuity is real, and it’s between completely different server-rendered HTML documents.

This is the missing piece for the routing-belongs-to-the-server argument made in Chapter 26. The historical case for client-side routing was the visual continuity it provided — the page didn’t reload, so the user’s sense of place wasn’t disrupted. View Transitions provides the visual continuity for server-rendered navigation. The page reloads. The navigation is a real server request. The user sees a smooth transition.

The architectural implications are substantial:

The application can use real <a href="..."> links. The browser handles navigation. The server returns HTML. View Transitions handles the visual continuity.

No client-side router is required for visual continuity. The historical reason for the router (smooth transitions) is now provided by the platform.

Progressive enhancement is preserved. If JavaScript fails to load, the View Transition doesn’t run, but the navigation still works — the new page just appears without the animation. The application degrades gracefully.

HTTP caching applies. Each URL is a real resource the browser and intermediate caches can store. View Transitions don’t interfere with caching.

The user can share links, bookmark pages, use the back button, and rely on browser autofill, all natively. None of these require the application to reimplement them.

The arguments in favor of SPA routing get substantially weaker. The arguments in favor of the platform’s navigation model — described at length in Chapter 26 — get substantially stronger.

This is the ergonomic loop the routing-belongs-to-the-server argument needed. The platform now provides smooth navigation animation as a native capability. The historical justification for moving away from the platform’s navigation is gone.

A related capability shipped through 2024 and is reaching cross-browser support in 2025: scroll-driven animations.

Animations can now be driven by scroll progress instead of by time:

@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
.card {
animation: fade-in linear;
animation-timeline: view();
}

The card’s opacity now interpolates based on its scroll position within the viewport, not based on elapsed time. The animation runs as the user scrolls, runs backward when the user scrolls back up, and pauses when the user stops scrolling. The platform handles the scroll-progress observation and the animation-progress mapping.

Before scroll-driven animations, this kind of effect required a JavaScript scroll listener, manual position calculation, and requestAnimationFrame loops. The CSS version is declarative, runs on the browser’s compositor (so it doesn’t block the main thread), and is significantly more efficient than the JavaScript equivalent.

For applications with rich scroll-based interactions — parallax effects, sticky reveals, scroll-triggered animations, progress indicators tied to scroll position — the platform now does the heavy lifting. The work that previously required libraries (ScrollMagic, Locomotive Scroll, GSAP’s ScrollTrigger plugin) is now native.

The platform has matured significantly. The libraries haven’t disappeared, because some kinds of animation work still benefit from what they provide.

GSAP (the GreenSock Animation Platform, by Jack Doyle) remains the gold standard for complex sequence animation. GSAP’s timeline API lets the developer compose long sequences of coordinated animations — different elements moving at different times with different easings, all synchronized to a single timeline. The library’s tooling (the gsap.timeline() and tween APIs) is more ergonomic for this kind of work than the equivalent platform code would be. Doyle’s team has spent fifteen years polishing the API; the experience shows.

GSAP also handles a long tail of animation features that the platform doesn’t cleanly cover. Morphing SVG paths. Custom easing functions. Physics-based motion. Plugins for specific use cases (text effects, SVG drawing, scroll-trigger sophistication). For applications where animation is the product — interactive storytelling, advertising, complex marketing experiences, certain kinds of data visualization — GSAP earns its keep.

Framer Motion is the React-native answer for animations bound to component lifecycle. The library’s design integrates animations with React’s reconciliation model in ways that pure CSS and WAAPI don’t quite match. For React applications where animations are tied to component mount/unmount and props changes, Framer Motion is often the most ergonomic option. The library remains alive and well, now under Vercel’s umbrella, with the Motion (originally Motion One) project as its smaller framework-agnostic sibling.

Motion One / Motion (Matt Perry, the same designer as Framer Motion) is a smaller library that exposes a CSS-driven API for animation with a Promise-based completion model. The library deliberately leans on platform primitives — much of what Motion does is convenience over the Web Animations API — and the result is significantly smaller than GSAP or Framer Motion. For teams that want a slightly nicer authoring API than raw WAAPI without the cost of a heavy library, Motion is the modern choice.

The pattern across all of these is the one this book has named several times. Libraries earn their place when they decorate platform capabilities ergonomically, and lose their place when the platform absorbs what they were doing. GSAP earns its place for complex sequence work. Framer Motion earns its place for React-bound component animations. Motion earns its place for a slightly-nicer WAAPI. The smaller, more general animation work that used to require these libraries can now be done natively.

The principle: check whether the platform now does what you’d reach for a library to do. The answer in 2025 is more often than you’d expect.

Animation has accessibility consequences that the chapter has to address directly.

Some users experience motion sickness or vestibular issues. Movement on screen can be disorienting, nauseating, or genuinely harmful. The operating-system level reduce motion preference (prefers-reduced-motion in CSS terms) is the standard signal for applications to scale back or eliminate non-essential motion.

@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

A reasonable baseline: when reduced motion is requested, animations and transitions complete almost immediately, parallax stops, scroll-based effects switch to a static presentation. The user can still see the application’s states; they just don’t see the movement between them.

View Transitions also need to be motion-conscious. A page transition that involves significant movement might need a prefers-reduced-motion override that uses a simple cross-fade or no animation at all. The platform doesn’t enforce this; the application is responsible for honoring the preference.

GSAP, Framer Motion, and Motion all expose ways to check prefers-reduced-motion and adjust their animations. CSS-driven animation can honor the preference through the media query. The architectural rule: if you’re animating something, check whether the user wants the animation.

This isn’t a niche concern. The percentage of users who have reduced motion enabled is non-trivial — somewhere between 5% and 20% depending on the user population and the device. The cost of honoring the preference is low. The cost of ignoring it includes lost users.

A forward-looking note that connects to the book’s hook chapter.

The future-of-UIs argument the introduction makes — that AI-generated, personalized, dynamic interfaces will increasingly become normal — has implications for animation. The interfaces will be generated. Each user might see a different UI for a given task. The motion design has to be coherent across an enormous space of possible UI shapes.

Platform-driven animation handles this in a way library-driven animation doesn’t. A View Transition automatically animates whatever the new UI is, against whatever the old UI was, using the matching rules the application has declared. The animation doesn’t have to be programmed for each specific UI shape; the platform handles the matching and the interpolation.

For AI-generated interfaces, this matters. Animation as a platform capability scales to interfaces nobody hand-authored. The AI generates the markup; the platform animates the transitions. The architecture stays coherent without the AI having to also generate animation timelines.

This is one of the structural reasons the platform-first argument matters for the AI generation specifically. Layers of framework-bound animation logic don’t compose with stochastic UI generation. Platform-driven animation does.

The next chapter takes the accessibility-as-platform-contract argument seriously — the position that accessibility isn’t a feature you add but a structural commitment the platform makes and that applications either honor or break. The chapter walks through the WAI, WCAG, the accessibility tree, ARIA done right, and the human story of the people who’ve made accessibility infrastructure exist over the past three decades.

Take a small site you’ve built or have access to. Three or four pages, real navigation between them, server-rendered HTML.

Add the cross-document view transition opt-in:

@view-transition {
navigation: auto;
}

Open the site in a recent Chrome or Safari. Navigate between pages. Notice the smooth cross-fade between the old and new pages.

Now add element matching. Pick an element that exists on multiple pages — a logo, a hero image, a primary heading. Give it a view-transition-name:

.site-header {
view-transition-name: header;
}

Navigate again. Notice that the header now animates between its positions on the two pages, instead of cross-fading.

Add a second matched element — maybe a card that appears as a small thumbnail on one page and as the main content on another. Give them the same view-transition-name. Watch the transition.

Add prefers-reduced-motion handling:

@media (prefers-reduced-motion: reduce) {
@view-transition {
navigation: none;
}
}

Toggle the reduced-motion preference in your browser’s dev tools and verify the transitions disappear.

Reflect on:

  1. How much JavaScript did this take? (Hopefully zero, if your links are real <a href> elements.)
  2. How does the transition compare to what a similar SPA would do? (Smoother, in many cases, because the platform’s animation engine runs on the compositor.)
  3. What would the equivalent client-side-routing implementation cost in code, dependencies, and complexity? (A router library, route components, layout shared elements, manual FLIP-pattern code or Framer Motion’s layoutId.)
  4. What would happen if you wrote this same site as a Next.js app with the App Router and React Server Components? Would the user-visible result be measurably better than this version? (Probably not, for a content-shaped site. Possibly worse for the first paint.)

The exercise is the strongest argument the chapter can make. Server-rendered navigation, with View Transitions, is no longer the slow option. The platform now gives you the smoothness the SPA generation was working around the platform to achieve. The architectural premise behind a huge fraction of the modern frontend ecosystem — we need a single-page application because server navigation is too jarring — is now obsolete for the cases where it always was the marginal argument.