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
collectAllViewLeaveAnimations → collectNestedViewAnimations 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
Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
No
Description
animate.leaveruns when the@ifthat removes the element and the element itself are in the same template. When the@ifremoves 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:
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 1sfade-outkeyframes (defined globally inindex.html, so style encapsulation plays no part). The only difference is which side of a component boundary the animated element sits on.npm install && npm start, then toggle each one off. The page logs every class change and everyremoval with a MutationObserver, so the difference is visible without watching the animation.
Expected
Both A and B keep the
leavingclass for the length of the animation, then are removed.Actual
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
collectAllViewLeaveAnimations→collectNestedViewAnimationswalks the removed node's children and its embedded views only: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@ifinside 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
Please provide the environment you discovered this bug in (run
ng version)Anything else?
No response