Skip to content

[popups] Keep position transition running when the rendered side changes#5016

Draft
atomiks wants to merge 5 commits into
mui:masterfrom
atomiks:claude/elated-jepsen-fb20fd
Draft

[popups] Keep position transition running when the rendered side changes#5016
atomiks wants to merge 5 commits into
mui:masterfrom
atomiks:claude/elated-jepsen-fb20fd

Conversation

@atomiks

@atomiks atomiks commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Fixes animation bugs that occur when the rendered side changes while moving the popup between detached triggers (e.g. one trigger's popup fits below it, the other's flips above): the position jumped instead of transitioning, and the size morph snapped instead of animating. Same-anchor collision flips remain instant.

Position jump on anchor change

With a Viewport present, the adaptiveOrigin middleware expresses the position using the inset property nearest the anchor (bottom for the top side, right for the left side, otherwise top/left) so size transitions stay pinned to the correct edge. When the rendered side changes, the inset property swaps (e.g. bottomtop), and CSS can't interpolate from auto, so the transition never starts and the popup snaps. Side changes that don't involve top/left keep the same properties and animate fine, which is why it only happened "sometimes".

Fix: the middleware tracks how each floating element was last positioned (inset pair, rendered side, dimensions, and anchor) in a WeakMap. When the rendered side changes or the inset pair swaps, it commits an intermediate state before the new styles land, so the outcome doesn't depend on how updates batch within a frame:

  • While the popup is moving to a new anchor (or any inset transition is in flight, which covers collision flips from interim updates mid-move), it commits the current visual position expressed in the new properties — resolved from computed insets, which include mid-transition values — so the transition continues from where the popup is. On a swapped axis it compensates for the positioner size snap that precedes repositioning, so the visible content starts exactly from its last painted position rather than detaching.
  • Otherwise (a steady-state collision flip, e.g. while scrolling), it commits the target position with transitions disabled, so the flip applies instantly instead of gliding across the anchor. This is required even without a property swap: updates that batch within one frame can otherwise interpolate toptop straight across the flip.

Size morph snap

usePopupAutoResize applied the side-dependent anchoring styles inside its measurement effect, with anchoringStyles as a dependency. A rendered side change between top and bottom (or left and right) changed that memo's identity, so the effect restarted: the cleanup cancelled the pending animation frame that would have started the size transition, and the re-run applied the already-committed new dimensions as the "previous" size — snapping the popup to the new size instantly.

Fix: the anchoring styles are applied in their own effect, so a side change no longer restarts the measurement cycle and the in-flight width/height transition continues.

Bundle size

The middleware is now registered through the store by the Viewport part (replacing the hasViewport boolean) instead of being imported statically by each positioner, so adaptiveOrigin is tree-shaken away unless a Viewport is used. DEFAULT_SIDES moved into useAnchorPositioning so the positioning hook no longer pulls in the middleware module. Navigation Menu still imports it directly as before.

Tests

  • Chromium test moving between two triggers whose popups render on opposite sides, asserting the positioner's position never changes abruptly between frames (continuity through the interim settle updates).
  • Chromium test asserting a same-anchor collision flip (scroll inside a clipping container) lands on the target instantly with inset transitions present.
  • Chromium test asserting the popup's height keeps transitioning through a side change (fails without the auto-resize fix: instant 140px snap).
  • Chromium test asserting the visible popup stays attached (frame-to-frame continuity of its rect) when a trigger change swaps the side with different content sizes.
  • A popover-side-switch experiment is included for manual verification of the combined behavior.

Notes

One remaining (pre-existing) discontinuity: when the side flips mid-morph, the popup's pin within the positioner switches edges (bottom: 0 ↔ static flow), so the popup can shift within the positioner box by the difference between the box height and the popup's current animated height at that moment. Smoothing that would require either transitioning the positioner's own width/height in demo CSS or compensating the pin swap, which is left out of scope here.

@atomiks atomiks added component: menu Changes related to the menu component. type: bug It doesn't behave as expected. component: tooltip Changes related to the tooltip component. component: popover Changes related to the popover component. component: preview card Changes related to the preview card component. labels Jun 11, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 11, 2026

Copy link
Copy Markdown

commit: 9f286ca

@code-infra-dashboard

code-infra-dashboard Bot commented Jun 11, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+812B(+0.17%) 🔺+285B(+0.19%)

Details of bundle changes

Performance

Total duration: 1,241.18 ms -76.40 ms(-5.8%) | Renders: 50 (+0) | Paint: 1,887.63 ms -105.62 ms(-5.3%)

Test Duration Renders
Mixed surface mount (app-like density) 93.73 ms 🔺+19.88 ms(+26.9%) 5 (+0)
Checkbox mount (500 instances) 67.68 ms ▼-21.74 ms(-24.3%) 1 (+0)

10 tests within noise — details


Check out the code infra dashboard for more information about this PR.

@netlify

netlify Bot commented Jun 11, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 9f286ca
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6a2a78f3316626000848e981
😎 Deploy Preview https://deploy-preview-5016--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the PR: out-of-date The pull request has merge conflicts and can't be merged. label Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: menu Changes related to the menu component. component: popover Changes related to the popover component. component: preview card Changes related to the preview card component. component: tooltip Changes related to the tooltip component. PR: out-of-date The pull request has merge conflicts and can't be merged. type: bug It doesn't behave as expected.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant