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
@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);
}
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
Template
<ng-container *movePresence="isOpen()">
<div moveEnter="fade-up" moveLeave="fade-down" [moveDuration]="300">Panel</div>
</ng-container>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.
@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;
}
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