Angular Route Transition Animations with the View Transitions API [2026]

Link copied
Angular Route Transition Animations with the View Transitions API [2026]

Angular Route Transition Animations with the View Transitions API [2026]

Route animations used to be one of the most involved things you could build in Angular. You wrapped the <router-outlet> in a trigger, tagged routes with animation data, and wrote query(':enter'), query(':leave') and group() sequences that positioned two pages on top of each other while they animated. The browser now does most of that work: the View Transitions API snapshots the old page, lets the framework update the DOM, and animates between the two. Angular's router integrates with it in one line.

This is lesson 13.3 of the Angular Tutorial. It builds on lesson 13.2, which covered enter, leave and state animations with CSS, and on the router essentials from Module 5. You'll learn how withViewTransitions() works, how to style transitions with the ::view-transition-* pseudo-elements, how to animate shared elements between pages with view-transition-name, how to customise or skip transitions with onViewTransitionCreated, and how to handle browser support and reduced-motion preferences.

How view transitions work #

When a navigation happens with view transitions enabled, the router runs the route deactivation and activation inside document.startViewTransition(). The browser then:

Step What happens
1. Capture The browser takes a snapshot of the current page (and of every element with a view-transition-name)
2. Update Angular deactivates the old route and renders the new one; the DOM changes as usual
3. Capture again The browser records the new state of the page and named elements
4. Animate It builds a tree of pseudo-elements showing the old and new snapshots, and animates between them (a cross-fade by default)
5. Clean up The pseudo-elements are removed and the real new page is shown

Your components don't animate anything. The animation happens on snapshots, in a layer above the page, which is why one line of configuration gives you a working default.

Enabling it #

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withViewTransitions } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes, withViewTransitions())],
};

Every router navigation now cross-fades. If the browser doesn't support the View Transitions API, the router skips it and performs the navigation as usual, with no error and no animation, so this is safe to turn on everywhere.

The router's view transition integration is marked developer preview in the Angular documentation, so its options may still change between versions. For applications that use RouterModule.forRoot, the equivalent is the enableViewTransitions: true option.

withViewTransitions() takes an options object:

Option What it does
skipInitialTransition Skips the transition for the initial navigation when the app first loads
onViewTransitionCreated A callback that receives the ViewTransition plus the route snapshots it's moving from and to; it runs in an injection context

Styling the transition #

The browser creates a pseudo-element tree for each transition:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)   ← snapshot of the outgoing page
      └─ ::view-transition-new(root)   ← live view of the incoming page

Override their animations to replace the cross-fade. These rules must go in your global stylesheet (src/styles.css), not in component styles: the pseudo-elements belong to the document, and Angular's view encapsulation would scope component styles so they never match.

/* src/styles.css */
::view-transition-old(root) {
  animation: 200ms ease-in both slide-out;
}
::view-transition-new(root) {
  animation: 250ms ease-out both slide-in;
}

@keyframes slide-out {
  to { opacity: 0; transform: translateX(-24px); }
}
@keyframes slide-in {
  from { opacity: 0; transform: translateX(24px); }
}

Keep these short. During the transition, the pseudo-element layer sits above the page, so a long transition delays the moment users can interact with the new page.

Shared-element transitions with view-transition-name #

The most striking use of view transitions is a shared element: a product image in a list that grows into the hero image on the detail page. Give the element the same view-transition-name on both pages, and the browser animates its position and size between them:

<!-- product-list.html -->
@for (product of products(); track product.id) {
  <a [routerLink]="['/products', product.id]">
    <img [src]="product.thumb" [alt]="product.name"
         [style.view-transition-name]="'product-' + product.id" />
    {{ product.name }}
  </a>
}
<!-- product-detail.html -->
<img class="hero" [src]="product().image" [alt]="product().name"
     [style.view-transition-name]="'product-' + product().id" />

Each view-transition-name must be unique among the elements on the page at the moment a snapshot is taken. If two visible elements share a name, the browser abandons the transition. That's why the list uses a name per product rather than one fixed name for all thumbnails. Names follow CSS identifier rules, so derive them from IDs that are safe as identifiers (letters, digits, hyphens), or prefix them as above.

You can style a single named group too, for example to slow down only the shared image:

::view-transition-group(*) {
  animation-duration: 250ms;
}

Customising transitions per navigation #

onViewTransitionCreated gives you the ViewTransition object for each navigation, so you can skip it, wait for it, or mark the document so CSS can choose an animation. Because it runs in an injection context, you can call inject() inside it.

A common pattern is to let routes declare their transition style in data, and expose it to CSS as an attribute on <html> for the duration of the transition:

// app.routes.ts
export const routes: Routes = [
  { path: '', component: Home },
  { path: 'products/:id', component: ProductDetail, data: { transition: 'zoom' } },
  { path: 'settings', component: Settings, data: { transition: 'none' } },
];
// app.config.ts
provideRouter(
  routes,
  withViewTransitions({
    onViewTransitionCreated: ({ transition, to }) => {
      let route = to;
      while (route.firstChild) route = route.firstChild;   // deepest activated route
      const kind = route.data['transition'] ?? 'slide';

      if (kind === 'none') {
        transition.skipTransition();
        return;
      }

      const root = inject(DOCUMENT).documentElement;
      root.dataset['transition'] = kind;
      transition.finished.finally(() => delete root.dataset['transition']);
    },
  }),
),
/* src/styles.css */
html[data-transition='zoom']::view-transition-new(root) {
  animation: 250ms ease-out both zoom-in;
}
@keyframes zoom-in {
  from { opacity: 0; transform: scale(0.96); }
}

transition.skipTransition() cancels the animation but not the navigation; the DOM update still happens. Use it for navigations that shouldn't animate, such as changes to query parameters or fragments within the same page, or routes where an animation would be distracting.

Browser support and fallback #

The router only starts a view transition when document.startViewTransition exists. In other browsers, navigation works exactly as it would without the feature. That makes view transitions a progressive enhancement: design pages so they make sense without the animation, and treat the animation as polish. Support for same-document view transitions has spread across the major browser engines, but check current data, for example on MDN, against the browsers your users have before relying on a shared-element effect to communicate something important.

Accessibility #

Two concerns apply to every route animation.

Reduced motion. Users who set "reduce motion" at the operating system level should not get sliding or zooming pages. A short cross-fade is generally acceptable; movement is the problem. Handle it in CSS, which covers every transition at once:

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

Alternatively, skip the transition entirely in onViewTransitionCreated when matchMedia('(prefers-reduced-motion: reduce)').matches is true. Lesson 13.4 makes this part of a reusable setup.

Focus and announcements. A view transition is purely visual; it doesn't move focus or announce the new page to screen readers. Keep whatever focus management and page-title updates you already have for navigation.

View transitions versus legacy route animations #

Legacy route animations withViewTransitions()
Package @angular/animations (deprecated) Router + browser API
Setup Trigger on the outlet wrapper, route data, query/group choreography One provider, global CSS
Old and new pages in the DOM at once Yes, you position them yourself No, the browser animates snapshots
Shared elements Hard to build view-transition-name
Unsupported browsers Works wherever the package runs Navigates without animation
Styling location Component metadata Global stylesheet

Gotchas #

Symptom Cause Fix
Custom ::view-transition-* styles ignored Rules in a component stylesheet Move them to src/styles.css
Transition silently doesn't happen Two visible elements share a view-transition-name Make names unique, for example product-<id>
No animation in some browsers View Transitions API unavailable Expected fallback; navigation still works
Every query-param change animates Transition runs for all router navigations Call transition.skipTransition() for those navigations
Page feels unresponsive after clicking a link Long transition keeps the snapshot layer on top Keep durations around 200–300 ms
Users with reduced motion still see slides No reduced-motion handling prefers-reduced-motion media query or skipTransition()

What's next #

Lesson 13.4 closes Module 13 by making animations reusable and safe: shared CSS animation utilities, prefers-reduced-motion across the app, disabling animations in tests and on request, how animations interact with OnPush and zoneless change detection, avoiding flashes with SSR and hydration, and keeping animations on properties the browser can animate cheaply.

Try it yourself #

Add withViewTransitions() to an app's provideRouter call and navigate around to see the default cross-fade. Then pick one list-to-detail flow and give the list thumbnail and the detail image a matching view-transition-name. Test it with "reduce motion" switched on in your operating system settings, and confirm that the movement disappears.

YouI enabled withViewTransitions() and the default fade works, but my ::view-transition-old(root) slide animation in app.component.css does nothing.
Claude · used search_documentationThe view transition pseudo-elements belong to the document root, not to any component. Angular’s view encapsulation rewrites the selectors in app.component.css with component-specific attributes, so they never match ::view-transition-old(root). Move the ::view-transition-old(root), ::view-transition-new(root) and their @keyframes into src/styles.css. While you’re there, add a @media (prefers-reduced-motion: reduce) block that sets animation: none on the view transition pseudo-elements, so the slide doesn’t play for users who’ve asked for less motion. Note that the router integration is in developer preview, so check the release notes when you upgrade.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied Angular When Angular is launched ? February 8, 2024 · 1 min read When was Angular launched? # Angu…

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

1 comment

1 thought on “Angular Route Transition Animations with the View Transitions API [2026]”

  1. Pingback: Angular Enter, Leave and State Animations with CSS [2026]

Leave a Comment

Your email stays private. Required fields are marked *

1 thought on “Angular Route Transition Animations with the View Transitions API [2026]”

  1. Pingback: Angular Enter, Leave and State Animations with CSS [2026]

Leave a Comment

Your email stays private. Required fields are marked *