skip to content

Legacy Trigger DSL

The deprecated @angular/animations package defines triggers with state, style, transition and animate, plus query and stagger for groups. Interviewers still ask it, and how to migrate to CSS.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Angular's legacy @angular/animations package, how do trigger(), state(), style(), transition() and animate() work together to animate a panel open and closed?

level: juniorimportance: must knowfreq 45%

answer

  1. named trigger bound with @
  2. states hold resting styles
  3. transitions match state changes
  4. animate takes duration delay easing
  5. a provider must be registered

basics

~20 s

trigger() names an animation bound in the template as [@name]; state() pairs a state value with the style() it rests in; transition() matches a change such as 'open <=> closed' and runs animate() steps. It needs provideAnimationsAsync() or an equivalent provider.

solid answer

~40 s

In the component's `animations` metadata I declare `trigger('openClose', [...])`. Inside it, `state('open', style({height: '*', opacity: 1}))` and `state('closed', style({height: '0px', opacity: 0}))` define the styles the panel keeps while it rests in each state. `transition('open <=> closed', [animate('250ms ease-out')])` says how to move between them; `animate()` takes a timing string of duration, optional delay and easing. In the template I bind `[@openClose]="open() ? 'open' : 'closed'"`; whenever that value changes, Angular picks the first matching transition and animates. The application must register `provideAnimationsAsync()`, `provideAnimations()` or `BrowserAnimationsModule`, otherwise the `@` binding throws an "unexpected synthetic property" error. The whole package has been deprecated since v20.2 in favour of CSS with `animate.enter` and `animate.leave`.

code

ts · 25 lines
ts
import {Component, signal} from '@angular/core';
import {animate, state, style, transition, trigger} from '@angular/animations';

@Component({
  selector: 'app-faq-panel',
  template: `
    <button type="button" (click)="open.set(!open())">Details</button>
    <div class="panel" [@openClose]="open() ? 'open' : 'closed'">
      Shipping takes 3 to 5 working days.
    </div>
  `,
  animations: [
    trigger('openClose', [
      state('open', style({height: '*', opacity: 1})),
      state('closed', style({height: '0px', opacity: 0, overflow: 'hidden'})),
      transition('open <=> closed', [animate('250ms ease-out')]),
    ]),
  ],
})
export class FaqPanel {
  protected readonly open = signal(false);
}

// bootstrapApplication(App, {providers: [provideAnimationsAsync()]});
// provideAnimationsAsync comes from '@angular/platform-browser/animations/async'

go deeper

for a junior

Recall the pieces: trigger bound with [@name], state for resting styles, transition for the change, animate for timing, and the provider that must be registered.

for a middle

Explain matching: declaration order and first match, wildcards and void, boolean matching, and why '*' styles animate to computed sizes.

for a senior

Maintain legacy triggers safely: know the NG05105 failure, lazy versus eager providers, @.disabled for tests, and that the package is deprecated with removal intended for v23.

for a principal

Weigh the cost of keeping a deprecated runtime engine against migrating, and sequence the migration so no component mixes the old and new systems.

## What the legacy DSL is Before Angular 20.2, animations were written in a TypeScript **domain-specific language** from the `@angular/animations` package. You describe styles and timings as function calls in the component's `animations` metadata, and an animation engine plugged into the renderer turns them into Web Animations at runtime. The package is **deprecated since v20.2** (its deprecation notice states an intent to remove it in v23), but it is still in many production codebases, which is why interviewers ask how it works. ## The five building blocks | Function | Role | |---|---| | `trigger(name, [...])` | Names an animation and groups its states and transitions. Bound in the template as `[@name]`. | | `state(name, style)` | The styles an element **keeps while resting** in that state. | | `style({...})` | A set of CSS properties, written in camelCase or quoted (`backgroundColor` or `'background-color'`). | | `transition(expr, steps)` | Which state change runs which steps, for example `'open => closed'`. | | `animate(timing, style?)` | Animates to a style over time. The timing is `'duration delay easing'`, e.g. `'250ms 50ms ease-out'`, or a number of milliseconds. | ## How a change flows 1. The template binds the trigger: `[@openClose]="open() ? 'open' : 'closed'"`. 2. The bound value changes from `'closed'` to `'open'` during change detection. 3. The engine walks the trigger's transitions **in the order they are declared** and takes the **first** one whose expression matches. 4. It runs that transition's steps. An array of steps runs **in sequence**. 5. When the steps finish, the element keeps the styles of the destination `state()`. ## Transition expressions worth knowing - `'open => closed'` for one direction, `'open <=> closed'` for both. - `'* => closed'`: the `*` **wildcard** matches any state. - `'void => *'` and `'* => void'`: `void` means the element is not in the DOM; these have the aliases `:enter` and `:leave`. - `':increment'` and `':decrement'` for numeric values that went up or down. - Several expressions separated by commas, or a **function** `(from, to, element, params) => boolean` for custom matching. - Boolean bindings match `true`/`false` and also `1`/`0`, but not merely truthy or falsy values. Because the first match wins, put specific transitions **before** a catch-all like `'* => *'`. ## Wildcard styles A `style()` value of `'*'` means "whatever the element computes to right now". `state('open', style({height: '*'}))` animates to the panel's natural height, the classic reason teams chose this DSL, since CSS historically could not transition to `height: auto`. ## Registering the engine The DSL does nothing unless the animation engine is provided: - `provideAnimationsAsync()` from `@angular/platform-browser/animations/async` loads the animation renderer **lazily**, after bootstrap, so its code is not in the initial bundle. Passing `'noop'` disables animations. - `provideAnimations()` from `@angular/platform-browser/animations` loads it **eagerly**, for animations that must play immediately on load. - `BrowserAnimationsModule` is the NgModule equivalent; `provideNoopAnimations()` and `NoopAnimationsModule` keep the bindings working without motion, which is common in tests. Without any of them, the first `[@trigger]` binding fails with **NG05105**, *Unexpected synthetic property @openClose found*, which reminds you to add one of those providers. ## Callbacks and disabling - `(@openClose.start)` and `(@openClose.done)` fire with an `AnimationEvent` carrying `fromState`, `toState` and `totalTime`. - `[@.disabled]="true"` turns off animations on an element and everything inside it; on the root component it disables them application-wide. ## Mistakes interviewers listen for - **Forgetting the provider.** The code compiles, then the first render throws NG05105. - **Mismatched state names.** The bound expression must produce exactly the strings used in `state()` and `transition()`; `'Open'` does not match `'open'`. - **Catch-all first.** A leading `'* => *'` shadows every specific transition declared after it. - **Expecting CSS classes.** The engine writes inline styles; there is no `.open` class to inspect or override from a stylesheet. - **Treating it as current.** Recommending the DSL for new Angular 22 code is the clearest sign of stale knowledge. ## Where it stands in 2026 Every function above, and the providers, is marked `@deprecated 20.2` with the note "Use `animate.enter` or `animate.leave` instead". For new code the recommendation is native CSS: classes for states, CSS transitions for timing, and `animate.enter`/`animate.leave` for elements entering and leaving. Knowing the DSL is still necessary to read, maintain and migrate existing components.

  • What is the difference between provideAnimationsAsync() and provideAnimations()?
    Both register the legacy animation engine. `provideAnimationsAsync()` loads the animation renderer lazily after bootstrap, so its code stays out of the initial bundle; animations are not rendered until it has loaded. `provideAnimations()` includes it eagerly, which you need when an animation must play the moment the app starts. Both are deprecated since v20.2.
  • Two transitions, 'open => closed' and '* => *', both match a change. Which runs?
    The one declared first. The trigger evaluates its transitions in declaration order and takes the first match, so specific transitions must be listed before a catch-all `'* => *'`, otherwise the catch-all swallows every change.

A trigger is a railway timetable for one element: states are the stations it can wait at, and each transition is a scheduled route between two stations. The first route in the timetable that matches the journey is the one the train takes.

saying these in an interview costs you the question

  • Triggers work without registering any animations provider.
  • The most specific matching transition wins regardless of declaration order.
  • state() styles apply only while the transition is running.
  • A binding of true matches any truthy value such as a non-empty string.
  • @angular/animations is the recommended way to animate new Angular 22 code.
open as a page

In Angular's legacy animations DSL, what do the :enter and :leave aliases mean, and why does a :leave transition sometimes never run?

level: middleimportance: should knowfreq 38%

basics

~20 s

:enter is an alias for 'void => ' and :leave for ' => void', where void means the element is not attached. A :leave only runs when the element itself is removed; removed along with its parent, it disappears without animating unless the parent queries it.

open as a page

In Angular's legacy animations DSL, how do query(), stagger(), group() and sequence() combine to build a staggered list reveal?

level: middleimportance: should knowfreq 35%

basics

~20 s

Put a trigger on the list container; its transition uses query(':enter', ...) to find the new rows and stagger(60, [...]) to delay each row's animation. group() runs steps in parallel, sequence() one after another, and a step array is a sequence by default.

open as a page

Angular deprecated @angular/animations in v20.2; how would you migrate a legacy app's triggers, including a staggered list reveal, to native CSS?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Migrate component by component: states become CSS classes, timings become transition or animation properties, :enter/:leave become animate.enter/animate.leave, a stagger becomes an index-based delay. Never mix both systems in one component; drop the provider once the last trigger is gone.

open as a page

In Angular's legacy animations package, how do animation() and useAnimation() make an animation reusable, and how do its {{ }} parameters get values?

level: middleimportance: nice to knowfreq 18%

basics

~20 s

animation() defines a step list with {{ name }} placeholders, usually exported from a shared file; useAnimation() plays it inside a transition. Values come from params: defaults given to animation(), overridden by params passed to useAnimation(); a missing value throws.

open as a page