View transitions is a cool and powerful API we can use in our Angular application to create animated transitions between views to elevating the overall user experience and enhancing usability, resulting in positive user feedback.
Update
[2023-09-12] Angular router now supports View Transitions. (#73e4bf2)
Use Case
The use case in this article is a single page application built with Angular to demonstrate a portfolio of a user. As in most customer projects, I also set up and use a design system here. I took the 3rd version of the great Google’s Material Design.
The routing is configured so that I have 3 routes:
- The
list
route provides the user’s profile with different interactive project cards - The
detail
route is accessible via project card and provides details to a project - The
editor
route provides the project’s preview and an editor
I created 8 view transitions during (router) navigation to animate the state change with CSS.
Live Demo
The deployed demo application showed in the video above currently only works with the latest Chromium browser, because it is a new API. I recommend the Canary version.
Please check the browser support for the ViewTransition API (Can I Use?).
Source Code
The source for the demo application is available on GitHub: Source Code
⚠️ To run the application on your machine you have to clone, build and link the Angular repo
Motion Design
Motion Design is an important part of design systems that enhances the user experience of our web applications. When a user clicks on an interactive control, it triggers navigation from one view to another. The visualization of this transition as an animation gives the user enough time to comprehend the change in the user interface, ultimately improving the overall user experience. Without this animation, users may need more time to understand the switch between views, which can lead to poorer acceptance and thus a poorer user experience.
Learn more about motion design at Material’s design system.
CSS transitions, CSS animations,
Web Animation API, Angular animations and route transition animations provides great APIs to implement different navigation transitions.
In some cases this works perfect, but in some cases this results in complex animation definitions and complex DOM structure.
For these cases we can use the new View Transition API ✨
to simplify the animations.
How View Transition API works
Idea 💡
The focus in this article is on the animations of views while navigating between pages (routes). There are other types of animation that I may describe in another article. So let’s take a closer look at this idea before we get to the code.
The video above shows the basic idea of a transition from overview page (list
route) to the detail page (detail
route).
We can observe two transitions:
preview
card transition with an expand animation- A cross-fade animation on the rest of the page, let’s call it
root
Now it should be clear that we have the before state (old
state) and the after state (new
state) for two elements,
the preview
and the root
(everything except preview).
API
Let’s take a look at the API and use pseudo code to simplify the case.
Pseudo code to demonstrate an old state (::view-transition-old
) on the list
route:
<html> <!-- root -->
<body>
<list-page>
<list>
<preview></preview>
<preview class="active"></preview> <!-- preview -->
<preview></preview>
<preview></preview>
</list>
</list-page>
</body>
</html>
Pseudo code to demonstrate the new state (::view-transition-new
) on the detail
route:
<html> <!-- root -->
<body>
<detail-page>
<preview></preview> <!-- preview -->
</detail-page>
</body>
</html>
The browser needs information about the element we like to use for our transition. We can use CSS to assign a view transition name to an element. The name is the identifier and must be unique.
Definition of the view transition in CSS:
preview.active {
view-transition-name: 'preview'
}
root
is the view transition name for the root element (whole page).
A definition is not necessary, it’s a part of user agent stylesheet.
The view-transition-name
must be unique on a page otherwise the animation is ignored.
Let’s assume we’re on the list
route, we can run this code to run the transition:
|
|
Lines description:
- The browser captures the
old
elements in the line#1
asscreenshots
- The provided callback is called
- The line
#4
manipulates the DOM - The browser creates
new
elements after callback execution - The animation is running
Without further CSS definition the browser uses the cross-fade as a default animation.
In these steps, a pseudo element tree
structure like this is created:
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(preview)
└─ ::view-transition-image-pair(preview)
├─ ::view-transition-old(preview)
└─ ::view-transition-new(preview)
Now we can use CSS to make interesting transitions and apply the style to pseudo elements.
Angular & View Transitions
Angular will support view transitions in the next release. It’s already merged.
Just add withViewTransitions()
to router features array to enable view transitions. ✨
// app.config.ts
import {provideRouter, withViewTransitions } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [provideRouter(routes, withViewTransitions())],
};
The View Transition API is new and there no support in Angular, but there is an issue #49401 to address that.
I forked the Angular repo and changed the router to demonstrate the view transitions in my demo application.
💡 I think the router is responsible for the invocation of the startViewTransition method during router navigation.
Workaround
You can try the View Transition API without changing Angular framework.
// list.component.ts
private readonly document = inject<Document & {startViewTransition: (callback: () => void) => {}}>(DOCUMENT);
navigate() {
this.ngZone.runOutsideAngular(() => {
if(!this.document.startViewTransition){
this.ngZone.run(() => {
void this.router.navigate(['detail', '42']);
});
}
});
}
Animation Implementation
Usually we define the animations in the components style to animate the HTML element inside the components.
In case of view transition, we want to apply animation style to the new pseudo element tree
.
For this reason I have defined the style globally and stored it in different style files.
src/styles/view-transitions
├─ app.component.transitions.scss
├─ app-bar.component.transitions.scss
├─ detail.component.transitions.scss
├─ editor.component.transitions.scss
├─ project-card.component.transitions.scss
├─ project-card.component.transitions.scss
└─ view-transitions.scss // index file
Expand / Collapse Animation
The video above shows an expand animation. The animation moves the preview part of the project card from small list item to large preview on the details page.
::view-transition-old (list
route)
The list
route contains a list of project cards. The current implementation of the view transitions supports only one element with same name.
The highlander principle There can be only one!
.
Therefore we have to assign the view-transition-name to the project card only when we click on the card. 💡 I place this code in the component to simplify the demo, but this logic should be part of the router.
// project-card.component.ts
async nav(segments: string[]): Promise<void> {
// add class to assign view-transition-name
this.elementRef.nativeElement.classList.add('active');
// startViewTransition, navigate to detail route and animate the state change
await this.router.navigate(segments);
}
// project-card.component.scss
&.active {
labs-project-preview {
view-transition-name: preview;
}
}
::view-transition-new (detail
route)
The target page component requires the same view-transition-name:
// detail.component.scss
:host {
labs-project-preview {
view-transition-name: preview;
}
}
Animation Style
The previous code enables the default cross-fade animation on both elements (root, preview). In the demo, however, you can see that the video is playing while it is transforming from an old state to the new state. To reach this behavior some style and code is necessary. The following stylesheet is a global style file that is applied to the pseudo elements.
|
|
- #2 The old preview screenshot is invisible, because we want to show and play the new preview video
- #7 Disable the default fade in effect
Workaround
The expand view transition should now look cool, but wait there is an issue.
There is an interruption while playing the video. The reason is that the video element in the preview
component is not the same HTML element.
The list
component contains a video element and the detail
component also contains its own.
There is a workaround, I created, but 💡 I think this should be part of the router too. The workaround should demonstrate the idea.
|
|
Micro Animations
Take a look at the video again, there are some micro animations such as back-button
with a slide in animation.
The animation style sheet looks like this:
// detail.component.transitions.scss
::view-transition-new(back-button):only-child {
animation: slide-in-from-left var(--view-transition-duration) ease;
}
@keyframes slide-in-from-left {
from {
transform: translateX(-200%);
}
to {
transform: translateX(0);
}
}
The slide-in-from-left
will be applied if the pseudo element ::view-transition-new(back-button)
is the only child in the image pair element ::view-transition-image-pair(back-button)
. It’s the parent element.
:only-child
is a pseudo-class that helps us to avoid the slide in animation between detail
and editor
route.
Both routes includes the back-button
.
Close Animation
I created the close animation to animate the transition from detail
to list
route.
// detail.component.transitions.scss
::view-transition-old(detail-page) {
animation: close-to-top-left-corner var(--view-transition-duration) ease;
}
@keyframes close-to-top-left-corner {
from {
clip-path: ellipse(100vmax 100vmax at center);
}
to {
clip-path: ellipse(.1rem .1rem at 2rem 4.5rem);
}
}
The animation can be easily implemented with the clip-path feature.
The definition of view-transition-name: detail-page
should be activated when you click the back button.
// detail.component.ts
navigateBack() {
this.elementRef.nativeElement.classList.add('full-page-transition')
void this.router.navigate([...]);
}
Special Close Animation 🚀
So far, a few simple standard animations had been implemented, time for something special! The CodePen below shows a full page transition. The USS Enterprise (NCC 1701) moves with warp 9 between views. ✨ This animation could be an alternative to the close animation.
And this is how the animation looks in the Angular application:
// app-bar.component.transitions.scss
::view-transition-old(special) {
mask: url("~assets/tail.svg") center top no-repeat,
url("~assets/space-landscape.svg") center center no-repeat,
url("~assets/ncc1701.svg") center center no-repeat;
mask-repeat: no-repeat;
mask-size: 10rem 30rem, 500% 500%, 100% 30%;
animation: hide-view-landscape 2.8s cubic-bezier(0.975, -0.005, 0, 1.02) forwards;
}
The basic idea is to use the old element (screenshot) and mask it with 3 images. Mask-position and transform properties are changed during CSS animation:
// app-bar.component.transitions.scss
@keyframes hide-view-landscape {
from {
mask-position: 50% 120%, 50% 0%, 50% 150%;
transform: scale(1);
}
45% {
transform: scale(1) translateY(-5%);
}
50% {
mask-position: 50% 120%, 50% 72%, 50% 60%;
}
80% {
mask-position: 50% -80%, 50% 140%, 50% -70%;
}
to {
mask-position: 50% -80%, 50% 140%, 50% -70%;
transform: scale(1) translateY(-10%);
}
}
Don’t forget the view transition name:
// detail.component.scss
:host {
.special & {
view-transition-name: special;
}
}
Reduce-Motion
I recommend to handle reduce motion when implementing animations.
Debugging
Debugging is easy because we can use the browser’s dev tools to debug animations and the pseudo-element tree.
Findings / Issues
Infinite Loop
You should avoid infinite
animations 🤣.
::view-transition-new(root) {
animation: infinite-loop 1s linear infinite; // Don't use infinite 😱
}
@keyframes infinite-loop {
from {
filter: blur(20rem);
}
to {
filter: blur(0);
}
}
Pseudo-Element tree is preserved after multiple reloads
[CLOSED] https://bugs.chromium.org/p/chromium/issues/detail?id=1434757
I created a temporary workaround to fix this issue during initial load / bootstrap of the Angular application:
setTimeout(() => {
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));
}, 10);
View Transitions with iFrames
In my case I use recorded videos to preview the projects. An alternative used by cool dev sites is the iframe. It allows you to preview a demo page with real code. Above I showed how to cache and reuse videos so that playback is not interrupted. This would not work with iframes, since they are re-executed when moved in the DOM.
Conclusion
To sum up, the View Transition API is a game-changer for developers looking to enhance their user interface with smooth and seamless animations.
With the ability to easily animate between views, developers can create a more engaging user experience without the need for complex DOM structures or Angular animations.
While the current implementation involves placing code in Angular application or patching the router,
the future holds even greater promise as the router takes over this functionality.
If you’re interested in learning more about View Transitions, Angular, Material, and CSS, be sure to follow me on Twitter for more insights and updates.