npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@ngspot/view-transition

v0.0.4

Published

> Easy animations in Angular using view-transition

Readme

view-transition

Easy animations in Angular using view-transition

MIT commitizen PRs styled with prettier

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:

  1. If the vtName directive is a child of the *vt directive, it only applies the view-transition-name CSS 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.
  2. It respects the vtNameForActive directive. More on it below.
  3. It sets view-transition-name to none during 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 *vt and vtName directives together has an additional benefit of cleaning up "enabling" only the view transitions associated with the change in *vt directive. Forgoing the usage of *vt directive means that the elements will always have the view-transition-name set 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 2 will be marked as active. This will switch the view-transition-name for corresponding rendered element from passive-item-2 to active-item-animation.
  • a change in items signal will trigger *vt directive 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 vtClass on that element if it was provided
  • add the class provided by the vtClassForActive directive

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.