View Transition API Integration in Angular—a brave new world (Part 1)

If you previously wanted to integrate view transitions into your Angular application, this was only possible in a very cumbersome way that needed a lot of detailed knowledge about Angular internals. Now, Angular 17 introduced a feature to integrate the View Transition API with the router. In this two-part series, we will look at how to leverage the feature for route transitions and how we could use it for single-page animations.

In diesem Artikel:

sl_300x300
Sascha Lehmann ist Developer bei Thinktecture. Sein Fokus liegt auf Angular und der Verbesserung von User Experience.

We can see a clear trend when we look at the current development of APIs on the web and their adoption in the browser landscape. The gap between native and web applications is getting smaller and smaller. Features such as the File System or Push API increasingly blur the boundaries and differentiation between web and native desktop or mobile applications. However, when it comes to mobile or cross-platform development, there is one specific area where web apps still have a disadvantage over natively developed apps: animations, or more precisely, container transforms, like the ones illustrated in the Material Design guidelines.

Native developers always had the advantage of dedicated animation APIs to accomplish this type of transform animations. But I don’t want to claim that these animations have never been possible on the web. It’s just the structure of the DOM and how navigations work on the web that made it very cumbersome—until now. 

So, let’s have a look at how view transitions work before we dive in. 

How view transitions work

The cool thing about view transitions is that you mainly control them via CSS and only need a little sprinkle of JavaScript. To trigger the transition, you call the native startViewTransition() function exposed by the document

				
					document.startViewTransition(async () => {
  // Change something in the DOM ...
});
				
			

When the function is called, the browser simply takes a screenshot of the current page. The function also accepts a callback function that notifies the browser when the desired DOM change has been executed. A further screenshot is taken, and the browser then cross-fades both images as a default. Et voila, you have successfully executed a view transition. But how is the crossfade accomplished, and how can I manipulate the animation using CSS? Therefore, there should be any accessible DOM elements, right? Yes, there are some DOM elements. But only only for the duration of the transition.

				
					::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)
				
			

After the screenshots are taken, the browser creates a pseudo DOM structure ::view-transition that sits on top of every other DOM element so it does not interfere with them or get accidentally overlaid. The ::view-transition represents the overlay root, the ::view-transition-group represents the root of a view transition, and the ::view-transition-image-pair represents the container of the old and the new screenshots. Each pseudo element can be separately styled using CSS. You can also “cut out” sub-parts of each screenshot to animate them separately by setting a view-transition-name via CSS on an element (note: each view transition name has to be unique). For further reading on the View Transitions API, look at the official MDN docs.

How to use/activate view transitions in the Angular Router

Next, I want to show you how to activate view transitions in Angular. I prepared a little app that will serve as a demonstration object. Nothing too fancy to keep everything nice and tidy. Just some image cards that navigate to a detail view when you click on them. Also, feel free to check out the repository and experiment with it. Currently, we use the default configuration, and the navigation between the card and detail view happens instantly.

So, let’s go to the app.config.ts file and add the following code snippet in the provider’s array.

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

				
			

We have successfully activated the view transition feature for the Angular router. Try it out, navigate between the card and detail views, and you will recognize the difference.

Note: If nothing changed for you, it could be because you currently don’t use a Chromium-based browser. Sadly, the support for the View Transition API has not yet arrived in all major browsers. But as luck would have it, Webkit announced that they added support for view transitions just a few days ago (April 10, 2024). So, view transitions will be available soon with one of the upcoming Safari releases. You can already try it out in Safari Technology Preview 192.

The navigation has changed its behavior from instant switching to a subtle cross-fade.  If the animation is too quick for you, just open the developer tools of your browser, go to the animations panel and slow it down. This is also the place to go if you want to debug any kind of animation. The cross-fade is already quite remarkable, but I think we can make the transition stand out even more.

Let's make this container transition pop

To achieve a first and straightforward container transition of the card image, we need it to be cut out of the initial screenshot and animated separately. We accomplish this by setting a view-transition-name CSS property on the element that gets clicked and on the corresponding image container in the detail view. Also some additional logic is needed to change the view-transition-name on the clicked item dynamically. But the result should look a bit like this. Let’s try it out.

Amazing, isn’t it? Just by “cutting out,” the browser adds another default transition animation to it, and you get this excellent effect. As mentioned before, you have full control over the animations. If you don’t like the default, just override it with your own. But as a first try, it looks already very neat.
If you look closely, you will probably notice that we have an issue. The transition animation only plays when we navigate “forward.” So, how do we also animate when we go back?

But how do we go back?

We also need to set the view transition names for the corresponding card elements to go back. More precisely, we need to know where we originally came from. There are different valid approaches to solving this problem. You could probably use some kind of state management, e.g., the new SignalStore, to store the element ID where we initially came from and apply the view-transition-name later on when we go back. Or, if you don’t like the idea of pulling another dependency in, we could go with the following approach.

The withViewTransitions() function we provided in the app.config.ts also accepts a callback function onViewTransitionCreated(), which gives us additional information about the animation. Among other things, from which URL we come and to which we go. We can use this information to set the view transition names. To access this information application-wide, we leverage a service to provide it. The result looks like this.

				
					// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(
      routes,
      withComponentInputBinding(),
      withViewTransitions({ onViewTransitionCreated }),
    ),
  ],
};

function onViewTransitionCreated(info: ViewTransitionInfo): void {
  const viewTransitionService = inject(ViewTransitionService);
  viewTransitionService.currentTransition.set(info);

  info.transition.finished.finally(() => {
    viewTransitionService.currentTransition.set(null);
  });
}

//view-transition.service.ts
@Injectable({
  providedIn: 'root',
})
export class ViewTransitionService {
  currentTransition = signal<ViewTransitionInfo | null>(null);
}
				
			

Now that we have the information available, let’s find a reusable way to utilise it in our application. To do so, we create a new ViewTransitionDirective that encapsulates the behavior of applying a view-transition-name when needed. 

				
					@Directive({
  selector: '[appViewTransition]',
  standalone: true,
  host: { '[style.view-transition-name]': 'viewTransitionName()' },
})
export class ViewTransitionDirective {
  private readonly viewTranistionService = inject(ViewTransitionService);

  protected readonly name = input<string>('', { alias: 'appViewTransition' });
  protected readonly id = input<number>();

  protected readonly viewTransitionName = computed(() => {
    const currentTransition = this.viewTranistionService.currentTransition();

    const apply =
      Number(currentTransition?.to.firstChild?.params['id']) === this.id() ||
      Number(currentTransition?.from.firstChild?.params['id']) === this.id();
      
    return apply ? this.name() : 'none';
  });
}
				
			

Each card has a unique ID used in the route parameters to access the corresponding member data. We simply apply the view-transition-name only when we navigate to or come from a URL that contains the same ID as our card element. Since we rely entirely on signals, we can encapsulate the logic in a computed and thus automatically ensure that the function is always re-evaluated as soon as one of the signals used inside of computed() changes. Usually, we manage the style binding with the help of the @HostBinding() decorator. As signals are not designed for combined use with decorators, we use the second option we have by leveraging the host property of the @Directive() decorator.

Now just apply the directive and its inputs in our HomeComponent and et voila! Forward and backward animations play seamlessly.

Conclusion

The examples showed how easy it is to activate view transitions in your Angular application and use them to lift your route animation game to the next level. Of course, there are still technical limitations regarding browser support. But even these will hopefully quickly disappear into thin air. 
In the second part of the series, we will examine how to use view transitions on a single page (e.g., for sorting animations) and how to use Angular features to make them as reusable as possible. Stay tuned!

GitHub Repository

You can find a sample implementation with all branches of the above examples right in this GitHub repository (all examples with Angular 17.3.0):

https://github.com/thinktecture/article-view-transition-api-angular

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Low-angle photography of metal structure
AI
cl-neu

AI-Funktionen zu Angular-Apps hinzufügen: lokal und offlinefähig

Künstliche Intelligenz (KI) ist spätestens seit der Veröffentlichung von ChatGPT in aller Munde. Wit WebLLM können Sie einen KI-Chatbot in Ihre eigenen Angular-Anwendungen integrieren. Wie das funktioniert und welche Vor- und Nachteile WebLLM hat, lesen Sie hier.
26.02.2024
Angular
sl_300x300

Konfiguration von Lazy Loaded Angular Modulen

Die Konfigurierbarkeit unserer Angular-Module ist für den Aufbau einer wiederverwendbaren Architektur unerlässlich. Aber in der jüngsten Vergangenheit hat uns Angular seine neue modullose Zukunft präsentiert. Wie sieht das Ganze jetzt aus? Wie konfigurieren wir jetzt unsere Lazy-Komponenten? Lasst uns gemeinsam einen Blick darauf werfen.
03.08.2023
Angular
YB_300x300

Using EntityAdapter with ComponentStore: @ngrx/entity Series – Part 3

As someone who enjoys the ComponentStore on an average level, I have written simple reactive CRUD logic several times. While storing a vast number of entities in the component state might not be a frequent use case, I will briefly illustrate the usage of the EntityAdapter with the @ngrx/component-store.
14.02.2023
Angular
YB_300x300

Multiple Entity Collections in the Same Feature State: @ngrx/entity-Series – Part 2

After introducing the @ngrx/entity package, I am often asked how to manage multiple entity types in the same feature state. While I hope that the previous part of this article series has made this more apparent, I will further focus on this question in the following.
07.02.2023
Angular
YB_300x300

Managing Your Collections With the EntityAdapter: @ngrx/entity-Series – Part 1

This three-part series of blogposts is targeted at developers who have already gained experience with NgRx but still manage their collections themselves. In the first part I introduce the Entity Adapter, in the second part I show you how to connect it to NgRx and in the third part how to do it with the Component Store as well.
31.01.2023
Angular
mm_300x300

Implementing Smart and Presentational Components with Angular: Condensed Angular Experiences – Part 4

In this article, we will explore how to apply the concept of smart and presentational components with Angular. We will choose a complex-enough target to see all aspects in action, yet understandable and within the scope of this article. The goal is to teach you how to use this architecture in your way. For that, we will iterate through different development stages, starting with the target selection and implementing it in a naive way. After the first development, we will refactor that naive solution into smart and presentational components that are reusable, refactor-friendly, and testable.
23.01.2023