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 this article:

SL-rund
Sascha Lehmann is Developer at Thinktecture. He focusses on web-based frontends with Angular and User Experience improvement.

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

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
Angular
SL-rund
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.
15.04.2024
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
.NET
KP-round
Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023