skip to content

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.