@ngspot/view-transition
v0.0.4
Published
> Easy animations in Angular using view-transition
Maintainers
Readme
view-transition
Easy animations in Angular using view-transition
Table of Contents
Demo:
https://dmitryefimenko.github.io/ngspot/view-transition
Prerequisites
You should be familiar with the View Transition API
Installation
NPM
npm install @ngspot/view-transition
Yarn
yarn add @ngspot/view-transition
Configure
import { onViewTransitionCreated } from '@ngspot/view-transition';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
appRoutes,
withViewTransitions({ onViewTransitionCreated }), // <-- Add this
),
// other providers
],
};In-page animations
All directives in the library has vt (view-transition) prefix.
Simple use-case
View Transition API animates changes made to the DOM. However, when using Angular, all DOM changes are performed by the framework behind the scenes as a response to the change in the state.
To accommodate this, the library provides a structural directive (*vt) that watches for the changes in the state and starts an animation on the elements containing view-transition-name CSS style when a change in state is detected.
@Component({
selector: 'ngs-vt-basic-demo',
imports: [...VIEW_TRANSITION_DECLARATIONS],
styleUrl: './basic-demo.component.scss',
templateUrl: './basic-demo.component.html',
})
export class MyComponent {
positionSignal = signal<'left' | 'right'>('left');
toggleShape() {
this.position.set(this.position() === 'left' ? 'right' : 'left');
}
}<ng-container *vt="positionSignal(); let positionValue">
<div
style="view-transition-name: position-animation"
class="{{ positionValue }}"
>
{{ positionValue }}
</div>
</ng-container>vtName directive
To simplify assignment of the view-transition-name CSS style, the library provides vtName directive.
<ng-container *vt="positionSignal(); let positionValue">
<div
vtName="position-animation"
class="{{ positionValue }}"
>
{{ positionValue }}
</div>
</ng-container>Besides shorter syntax, using vtName directive provides a few additional benefits:
- If the
vtNamedirective is a child of the*vtdirective, it only applies theview-transition-nameCSS style when change is detected and animation is about to run. After the animation is complete, the style is removed. This assures that different animations on the page do not collide with each other. - It respects the
vtNameForActivedirective. More on it below. - It sets
view-transition-nametononeduring the route navigation. For more information on this, see Route navigation animations.
Triggering View Transition Programmatically
Library provides ViewTransitionService, which has run() method.
Under the hood this method is called by *vt directive to schedule an animation.
In case you need to trigger animation manually, you can call this method for a change in state that you know will result in change in DOM:
class MyComponent {
private viewTransitionService = inject(ViewTransitionService);
isReady = signal(false);
setReady() {
this.viewTransitionService.run(() => {
this.isReady.set(true);
});
}
}@if (!isReady()) {
<div vtName="some-animation" class="not-ready">I'm not ready!</div>
} @else {
<div vtName="some-animation" class="ready">I'm ready!</div>
}Important!: Keep in mind that usting the
*vtandvtNamedirectives together has an additional benefit of cleaning up "enabling" only the view transitions associated with the change in*vtdirective. Forgoing the usage of*vtdirective means that the elements will always have theview-transition-nameset to the provided value - whether they are expected to be animated or not.
Targeting specific elements in a for loop
A lot of the times there is a need to give an element a distinct view-transition-name. This scenario is common for when dealing with an array of elements that already have a view-transition-name assigned, but an action on one of these elements is to be performed, which would require a change in view-transition-name for that element only.
To handle this scenario the ViewTransitionService provides a method: setActiveViewTransitionNames(...names: string[]). This goes together with vtNameForActive directive.
class MyComponent {
items = signal<Item[]>([
{ id: 1, name: 'item 1', ready: false },
{ id: 2, name: 'item 2', ready: false },
{ id: 3, name: 'item 3', ready: false }
]);
private viewTransitionService = inject(ViewTransitionService);
isReady = signal(false);
setReady() {
this.viewTransitionService.setActiveViewTransitionNames('passive-item-2');
this.items.update((items) => {
return items.map((item) => {
if (item.id === 2) {
return { ...item, ready: true }
}
return item;
});
})
}
}<ng-container *vt="items(); let itemsVal">
@for (let item of itemsVal; trackBy: item.id) {
<div
[vtNameForPassive]="`passive-item-` + item.id"
[vtNameForActive]="'active-item-animation'"
>
{{ item.name }}
</div>
}
</ng-container>With the code above, once setReady method is called, the following sequence of events will happen:
- the item with id
2will be marked as active. This will switch theview-transition-namefor corresponding rendered element frompassive-item-2toactive-item-animation. - a change in
itemssignal will trigger*vtdirective to start a new view transition. - after the animation is complete, the active item is disabled back to the passive mode.
Note, the vtNameForPassive directive is the same thing as vtName directive. It's only encouraged to use it for readability purposes together with vtNameForActive directive.
vtActiveGroupId directive
Directive provides a way to target multiple elements with vtName directive by a single string:
<div
[vtNameForPassive]="`passive-item-1`"
[vtNameForActive]="'active-item-animation'"
[vtActiveGroupId]="active-group"
>
Item 1
</div>
<div
[vtNameForPassive]="`passive-item-2`"
[vtNameForActive]="'active-item-animation'"
[vtActiveGroupId]="active-group"
>
Item 2
</div>this.viewTransitionService.setActiveViewTransitionNames('active-group');Calling setActiveViewTransitionNames in the example above will set view-transition-name CSS property on both elements to active-item-animation.
Targeting many elements in the loop
View Transition API supports targeting multiple elements via view-transition-class CSS style. See docs.
However, in order to avoid unnecessary view transition runs, there should be a maintainable way to assign a CSS class to an element right before the animation begins and remove it it when the animation is done. This library provides such functionality via vtClass directive:
<ng-container *vt="items(); let itemsVal">
@for (let item of itemsVal; trackBy: item.id) {
<div vtClass="my-item">
{{ item.name }}
</div>
}
</ng-container>.my-item {
view-transition-class: my-item;
}
// other styles targeting `my-item`vtClassForActive directive
When ViewTransitionService.setActiveViewTransitionNames(name) method is called, and the name matches to the value of the [vtName] directive on that element, the vtClassForActive directive will:
- remove the class that was set by
vtClasson that element if it was provided - add the class provided by the
vtClassForActivedirective
Route navigation animations
This library treats route navigation animations separately from the in-page animations. To achieve this, the vtName directive sets view-transition-name CSS style on the element to none when route navigation is in progress.
To enable animation on elements during route navigation, use vtNameForRouting and vtNameForRouterLink directives.
vtNameForRouting directive
Directive sets view-transition-name only when router navigation is in progress.
<main [vtNameForRouting]="main-content">
<router-outlet></router-outlet>
</main>vtNameForRouterLink directive
Similar to vtNameForRouting directive, vtNameForRouterLink directive only has a potential of setting view-transition-name when router navigation is in progress.
However, in addition to that, the directive is designed to be applied to an element that also has routerLink directive or to an element that is a child of an element with routerLink directive.
The directive will apply the view-transition-name CSS style only if the destination URL matches the URL produced by the routerLink directive.
<!-- Page rendering a list of images -->
@for (let img of images; trackBy: item.id) {
<a [routerLink]="img.id">
<img
width="200"
height="150"
vtNameForRouterLink="image"
alt="{{ img.title }}"
[src]="img.src"
/>
</a>
}<!-- Page rendering selected image details -->
<img
width="1024"
height="768"
vtNameForRouting="image"
alt="{{ image().title }}"
[src]="image().src"
/>See Route animation demo for full example.
Styling
As of today, all styles related to styling view transitions must be global.
Please consider adding the following styles to your global style sheet:
:root {
/* Optimization */
view-transition-name: none;
}
@media (prefers-reduced-motion: no-preference) {
/* Force flat mode, should the browser use layered by default */
* {
view-transition-capture-mode: flat;
}
/* Prevent mouse clicks during animations */
html::view-transition {
pointer-events: none;
}
}Styles for animations specific to a particular component should also be global. Use ::ng-deep to achieve this:
::ng-deep {
::view-transition-old(.card):only-child {
animation: scale-out .25s ease-out forwards;
}
}There are times when you need to add global styles, but with component-specific context. To achieve this, the library provides a vt-style component. Example of usage:
import { VIEW_TRANSITION_DECLARATIONS } from '@ngspot/view-transition';
@Component({
selector: 'ngs-card',
imports: [...VIEW_TRANSITION_DECLARATIONS],
template: `
<!-- Other component contents -->
<vt-style [vtStyle]="vtStyle()" />
`,
})
export class CardComponent {
element = input.required<IsotopeEl>();
vtStyle = computed(() => {
const ix = this.element().index;
const delay = ix * 20;
const vtName = `card-${ix}`;
return `
::view-transition-group(${vtName}) {
animation-delay: ${delay}ms;
}
`;
});
}See Isotope animation demo for full example.
