Skip to content

bug(select): exit fade resets opacity before deferred panel removal with legacy animations #33851

Closed

Description

@EthanSK

Is this a regression?

  • Yes, this behavior used to work in the previous version

Unknown. It was noticed recently, but I have not established the first affected Angular/Material or Chromium version.

Description

A mat-select panel can become opaque again after its closing fade has finished, before Angular actually removes its DOM node. The transient panel is visible as a flash in Chromium.

A reduced example uses only Angular, Material's prebuilt azure-blue theme, a standalone select, zoneless change detection, and provideAnimationsAsync(). Binding an empty legacy animation trigger on the root component activates the legacy animation renderer and reproduces the delayed removal / opacity rebound. There is no application filter logic, routing, custom select animation, or overlay configuration in this reduced case.

Reproduction

Complete isolated source and one-click StackBlitz launcher: https://gist.github.com/EthanSK/056299e55311004487e48e0a6f66b203

REPL link limitation: The official StackBlitz SDK launcher opens the complete project, but its anonymous generated URL is session-scoped until saved to an account, so the gist is the persistent entry point. I verified that the launcher opens the expected project files and begins dependency installation; remote dependency installation and browser preview have not completed in this test. CodeSandbox’s angular-cli preset unconditionally requires zone.js even though this reproduction is zoneless, and its generic Node sandbox did not start an Angular CLI preview. I did not add Zone.js because that would change the relevant configuration. The same component/providers were actually run and measured in a local Angular CLI build using the tested package versions.

  1. Run the supplied example in Chromium 153. Tested with 153.0.8010.48 on macOS 26.6.2.
  2. Open Choice, let the opening animation finish, then press Escape without changing the value. Repeat, leaving the page otherwise idle.
  3. For measurements, enable the optional diagnostics described in the reproduction. They sample animationend, its microtask, a zero-delay timer, and actual DOM disconnection. They do not run a continuous requestAnimationFrame loop.
  4. Compare with the empty animations: [trigger(...)] declaration and matching template binding removed, keeping provideAnimationsAsync() configured.
  5. Restore the trigger and add only the exit animation-fill-mode: forwards rule below.

Minimal component/provider configuration:

@Component({
  selector: 'app-root',
  imports: [MatFormFieldModule, MatSelectModule],
  animations: [trigger('activateLegacyRenderer', [])],
  template: `
    <h1 @activateLegacyRenderer>Material select close reproduction</h1>
    <mat-form-field>
      <mat-label>Choice</mat-label>
      <mat-select value="one">
        <mat-option value="one">One</mat-option>
        <mat-option value="two">Two</mat-option>
        <mat-option value="three">Three</mat-option>
      </mat-select>
    </mat-form-field>
  `,
})
class SelectReproductionComponent {}

bootstrapApplication(SelectReproductionComponent, {
  providers: [provideZonelessChangeDetection(), provideAnimationsAsync()],
});

Expected Behavior

The select remains transparent after its exit fade completes and until its DOM is removed.

Actual Behavior

The exit animation has fill mode none. When it finishes, computed opacity returns to 1 while the panel is still connected. The legacy animation renderer defers actual node removal, leaving an opportunity for the opaque panel to be painted.

Repeated measurements of the reduced example:

Configuration Result at the zero-delay callback after animationend
Zoneless + activated legacy renderer + default CDK overlay 10/10 still connected, opacity 1
Same, with usePopover: false 10/10 still connected, opacity 1
Remove the bound legacy trigger, retain async animation provider 10/10 already disconnected
Restore trigger and apply forwards fill 10/10 still connected, opacity 0

The baseline default-overlay animationend-to-removal interval was approximately 0.9–2.4 ms. Timing instrumentation can perturb scheduling; these measurements are not a frequency estimate of visible flashes. In the original application, consecutive browser screenshots captured a nearly transparent panel, then a fully opaque panel, then its removal. The reduced example reproduces the same connected/opaque state without application UI or styles.

A narrow workaround that preserves the fade and removal timing:

.mat-mdc-select-panel.mat-select-panel-animations-enabled.mat-select-panel-exit {
  animation-fill-mode: forwards;
}

This also prevented the measured rebound in 23/23 matched filter closes in the original application, and restoring the original CSS restored the rebound in 10/10 closes. Material menus already retain their final fade state and did not show this rebound in six control closes.

Environment

  • Angular core / animations / platform-browser: 21.2.10
  • CDK / Material: 21.2.8
  • Angular CLI / build: 21.2.8
  • TypeScript: 5.9.3
  • RxJS: 7.8.2
  • Browser directly instrumented: Chromium 153.0.8010.48 (embedded browser)
  • Also observed manually: Google Chrome 153.0.8010.52, including incognito
  • Operating system: macOS 26.6.2
  • Zoneless with provideAnimationsAsync() and an active legacy animation trigger

Related issues and scope

Chromium 153 has animation scheduling changes, but a browser feature toggle / version bisect has not been performed. I am not claiming that Chrome 153 introduced the bug or that it is a Chromium standards violation. Filing here because the select exit loses its final opacity before removal; please redirect to Angular core if the deferred cleanup is the preferred place to address it.

Activity

  1. EthanSK commented on Sep 22, 2026

    @EthanSK
    Author

    Follow-up from testing the suggested CSS workaround: it needs a rapid-reopen caveat.

    With animation-fill-mode: forwards on .mat-select-panel-exit, close a select with Escape and immediately reopen it with Enter before the 100 ms exit finishes. In five of five local trials on Material 21.2.8, the select reported aria-expanded="true" and its panel remained mounted, but the panel retained mat-select-panel-exit. After 100 ms, the forwards-filled exit held that open panel at opacity 0, so it was invisible. I reproduced this in the original app's select; I have not yet rerun this specific rapid-reopen case in the reduced StackBlitz project.

    In the installed select.mjs, open() calls _cleanupDetach?.() to cancel the prior exit listener/timer, but does not remove mat-select-panel-exit; _exitAndDetach() adds the class. Clearing the stale exit class from the reopened panel when its trigger becomes expanded restored visibility in 12/12 rapid reopens on one select, 10/10 on another, and four other select controls. Ordinary closing fades continued to finish invisibly and detach.

    So the fill-mode rule fixes the original close flash but is not by itself a safe complete workaround for a fast close/reopen. A library-side fix could also clear the exit class when reusing the panel; the appropriate upstream implementation is for maintainers to choose.

  2. self-assigned this
    on Sep 25, 2026
  3. added
    P3An issue that is relevant to core functions, but does not impede progress. Important, but not urgent
    and removed
    gemini-triagedLabel noting that an issue has been triaged by gemini
    on Sep 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

P3An issue that is relevant to core functions, but does not impede progress. Important, but not urgentarea: material/select

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions