Skip to content

Flyout: freeze content during the hide animation and animate it by default - #675

Merged
blvdmitry merged 5 commits into
mainfrom
flyout-frozen-exit-animation
Oct 8, 2026
Merged

blvdmitry merged 5 commits into
mainfrom
flyout-frozen-exit-animation

Conversation

@blvdmitry

@blvdmitry blvdmitry commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Brings back the Flyout hide animation for every component built on it, using the Suspense "freeze" technique from A React trick to improve exit animations.

New internal _private/Freeze component

  • While frozen, a sibling <Suspend /> suspends the boundary with no fallback. React keeps the DOM from the last commit and stops committing updates from props, state or context.
  • React hides suspended DOM with display: none !important. Freeze undoes this in a useInsertionEffect, the earliest point in the same commit, before paint.
  • Differences from the article's version:
    • Wrapper element instead of fragment refs: fragment refs are only in React canary, and the package supports React 18 and 19. A display: contents wrapper also stops React from blanking top-level text nodes and doesn't overwrite the children's own inline display.
    • Transition safety: a transition never commits an already-visible boundary falling back, so freezing inside startTransition would block that transition forever. If the frozen render doesn't commit within 100ms, Freeze gives up and renders the children normally. Covered by a story that fails without this.

Flyout

  • Flyout.Content wraps its children in <Freeze frozen={!active}>. active is now passed through context, so the freeze happens in the same render as the close.
  • Removed the forced disableHideAnimation from Select, DropdownMenu (which forced it for every non-hover trigger) and Autocomplete. The prop is still supported and still decides between animated and immediate removal.

Fixes for problems that turning the animation on exposed

  • A flyout closed before its open transition started got stuck mounted in the hidden state. Per the CSS transitions spec, the transition is cancelled instead of reversed, so transitionend never fires. On hide, Flyout now removes the content right away if it has no running animations. This already affected Popover.
  • Content that's animating out ignores pointer events, so a quick second click can't hit an option that's on its way out.
  • Focus is now returned to the trigger when the content is removed without an animation (reduced motion, or transitions disabled). Before, the trap was only released on hidden, unless disableHideAnimation was set. Select used to set it, so Select avoided this. Covered by a story that fails with the old condition.

Related Issue

None

Screenshots / Recordings

Visual change: Select, DropdownMenu and Autocomplete now animate out. Their content keeps its pre-close state while it animates out.

Notes for Reviewers

  • New stories: Internal/Freeze, plus three Flyout tests: content doesn't change while closing, close right after opening, and focus returns with transitions disabled.
  • Select tests now wait for the previous menu to finish closing, and for aria-expanded to turn false, since both now happen after the exit animation.
  • Ran the full storybook browser suite locally: all tests pass except Image › onLoad, which also fails on main in my sandbox because it loads a remote image. Unit tests, lint and typecheck pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TFwAYqyYv9BQJuc5zDGzgv

…fault

Adds an internal Freeze component that suspends its children without a
fallback, so React keeps their last committed DOM while ignoring updates
from props, state and context, and reveals the DOM React hides for the
suspended tree. Freezing in a transition, which would never commit, gives
up after a short timeout instead of blocking it.

Flyout content is frozen as soon as it stops being active, so Select,
DropdownMenu and Autocomplete no longer need to disable the hide animation.
disableHideAnimation is kept as an opt-out.

Also fixes issues surfaced by animating these components:
- aria-expanded reports the collapsed state as soon as the flyout closes
- content that's animating out ignores pointer events
- content closed before its open transition started is removed right away,
  since the cancelled transition never fires transitionend
- focus trap is released when the content is removed without animation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFwAYqyYv9BQJuc5zDGzgv
@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

size-limit report 📦

Path Size Loading time (3g) Running time (snapdragon) Total time
Library / JS 51.83 KB (0%) 1.1 s (0%) 6.6 s (-6.6% 🔽) 7.6 s
Library / CSS 23.8 KB (0%) 476 ms (0%) 0 ms (+100% 🔺) 476 ms
Theming / JS 7.78 KB (0%) 156 ms (0%) 602 ms (+137.52% 🔺) 757 ms
Theming with a default theme definition / JS 8.69 KB (0%) 174 ms (0%) 454 ms (-32.41% 🔽) 628 ms

@blvdmitry
blvdmitry marked this pull request as ready for review October 8, 2026 23:11
claude and others added 4 commits October 8, 2026 23:17
…with a sibling

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFwAYqyYv9BQJuc5zDGzgv
…aria attributes as before

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TFwAYqyYv9BQJuc5zDGzgv
@blvdmitry
blvdmitry merged commit d9770c5 into main Oct 8, 2026
7 checks passed
@blvdmitry
blvdmitry deleted the flyout-frozen-exit-animation branch October 8, 2026 23:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants