Skip to content

animate.leave is skipped when the removed node is a child component (leave class never applied) #70131

Description

@Arneball

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

animate.leave runs when the @if that removes the element and the element itself are in the same template. When the @if removes a component whose template contains the element, the leave animation is skipped: the class is never applied and the node leaves the DOM immediately.

The docs describe this case as working:

if animate.leave is placed on an element that is a descendent of the element being removed,
those child animations will happen before the parent node is removed from the DOM.
https://angular.dev/guide/animations

A component's rendered content is a descendant of the component element in the DOM, so B below is expected to behave like A.

Reproduction

Two boxes, the same animate.leave="leaving" on both, the same 1s fade-out keyframes (defined globally in index.html, so style encapsulation plays no part). The only difference is which side of a component boundary the animated element sits on.

// app.ts
@if (a()) {
  <div class="box" animate.leave="leaving">A: same template</div>
}
@if (b()) {
  <app-child />
}

// child.ts
@Component({
  selector: 'app-child',
  template: `<div class="box" animate.leave="leaving">B: inside a child component</div>`,
})
export class Child {}

npm install && npm start, then toggle each one off. The page logs every class change and every
removal with a MutationObserver, so the difference is visible without watching the animation.

Expected

Both A and B keep the leaving class for the length of the animation, then are removed.

Actual

24276ms  A: class -> "box leaving"
27074ms  B: removed from DOM

A is given class="box leaving" and stays in the DOM until the animation finishes. B never receives the class and is gone in the same frame the signal flips.

Probable cause

collectAllViewLeaveAnimationscollectNestedViewAnimations walks the removed node's children and its embedded views only:

if (subView[TVIEW].type === 2) {          // TViewType.Embedded
  collectAllViewLeaveAnimations(subView, collectedPromises);
}

A component's view is TViewType.Component (1), so it is never entered and the leave animations registered inside it are never found.

Why it matters

It rules out the natural shape for a reusable overlay/modal/dialog component: the caller owns the
open/closed state with an @if, and the component owns the element that has to animate out. The only workaround is to move the @if inside the component that owns the element, which pushes state that belongs to the caller down into the component.

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/stackblitz-starters-xuelsyfp?file=src%2Fmain.ts

Please provide the exception or error you saw

No exception or error is thrown — the failure is silent.

The observed difference, logged by a MutationObserver on each element's class attribute and on its
removal:

  24276ms  A: class -> "box leaving"
  27074ms  B: removed from DOM

A receives the leave class and its removal is deferred until the animation finishes. B is removed in
the same frame the signal flips, having never received the class.

Please provide the environment you discovered this bug in (run ng version)

Angular CLI       : 22.1.3
Angular           : 22.1.1
Node.js           : 26.7.0
Package Manager   : npm 11.19.0
Operating System  : darwin arm64

┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package                   │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build            │ 22.1.3            │ ^22.1.3           │
│ @angular/cli              │ 22.1.3            │ ^22.1.3           │
│ @angular/common           │ 22.1.1            │ ^22.1.0           │
│ @angular/compiler         │ 22.1.1            │ ^22.1.0           │
│ @angular/compiler-cli     │ 22.1.1            │ ^22.1.0           │
│ @angular/core             │ 22.1.1            │ ^22.1.0           │
│ @angular/forms            │ 22.1.1            │ ^22.1.0           │
│ @angular/platform-browser │ 22.1.1            │ ^22.1.0           │
│ @angular/router           │ 22.1.1            │ ^22.1.0           │
│ rxjs                      │ 7.8.2             │ ~7.8.0            │
│ typescript                │ 6.0.3             │ ~6.0.2            │
└───────────────────────────┴───────────────────┴───────────────────┘

Anything else?

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimegemini-triagedLabel noting that an issue has been triaged by gemini

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions