Skip to content
Documentation/Presence: animate things leaving

Search documentation

Type to search the documentation.

Guides

Presence: animate things leaving

Keep a view or a keyed list row mounted until its leave animation finishes, including nested children and rows that return mid-exit.

A leave animation needs an element to still exist. @if and @for remove nodes synchronously, so by the time moveLeave could run, the node is gone — the directive is not broken, there is simply nothing left. Presence directives own the lifetime instead: they hold the view, wait for every registered leave animation, and only then let Angular remove it.

A single view: *movePresence

panel.ts
@Component({
  selector: 'app-panel',
  imports: [MovePresenceDirective, MoveEnterDirective, MoveLeaveDirective],
  template: `
    <ng-container *movePresence="open()">
      <aside moveEnter="fade-left" moveLeave="fade-right">Settings</aside>
    </ng-container>
  `,
})
export class Panel {
  protected readonly open = signal(true);
}

View source

MovePresenceDirective waits for all descendants that registered a leave animation, including nested ones.

movePresence

movePresence keeps the view mounted until the leave animation finishes. Toggle it with the control or the button in the preview.

Panel

enter fade-up · leave fade-down

Inputs

Template

<ng-container *movePresence="isOpen()">
  <div moveEnter="fade-up" moveLeave="fade-down" [moveDuration]="300">Panel</div>
</ng-container>

movePresence — expanded preview

Toggle the panel with the control or the button in the preview. Open the full presence playground.

A keyed list: *movePresenceFor

MovePresenceForDirective is the list counterpart: each row stays mounted while it leaves, and a row that returns mid-exit is revived instead of duplicated.

rows.ts
@Component({
  selector: 'app-rows',
  imports: [MovePresenceForDirective, MoveEnterDirective, MoveLeaveDirective],
  template: `
    <ul>
      <li
        *movePresenceFor="let row of rows(); trackBy: byId"
        moveEnter="fade-up"
        moveLeave="fade-down"
      >
        {{ row.name }}
      </li>
    </ul>
  `,
})
export class Rows {
  protected readonly rows = signal<readonly Row[]>([{ id: 1, name: 'First' }]);
  protected readonly byId = (_: number, row: Row) => row.id;
}

View source

Important

Track by a stable identity, never by index. With track $index Angular reuses DOM nodes, so reordering does not replay enter animations and exit animations attach to the wrong row.

Plain @for (row of rows(); track row.id) is still right when rows never need an exit animation.

Exit variants

With moveVariants, moveExitVariant names the variant that plays inside *movePresence before removal. With [moveAnimation], use its exit state.

Related: movePresence playground · movePresenceFor playground · moveLeave playground