Skip to content

[popover] Positioner slides from viewport top-left on first open in React 17 #5187

Description

@mkhu-scc

Bug report

Current behavior

In React 17, opening a Popover for the first time via mouse click causes the positioner to animate from the viewport top-left (0, 0) to its target position, instead of appearing in place next to the trigger. 100% reproducible on the first open after mount

React 19 is not affected. @base-ui/react 1.4.1 + React 17 is not affected

Expected behavior

The positioner's initial position change (from translate(0,0) to translate(x,y)) should not trigger a CSS transition. The popover should appear directly beside the trigger.

Reproducible example

Screen.Recording.2026-07-08.at.19.04.08.mov

CodeSandbox

Base UI version

1.6.0

Which browser are you using?

Chrome

Which OS are you using?

Mac OS

Which assistive tech are you using (if applicable)?

N/A.

Additional context

Base UI guards the positioner's initial-position transition with an inline style: { transition: 'none' } that only lives while transitionStatus === 'starting' — one animation frame. The position itself is computed by @floating-ui/react-dom's useFloating as a microtask (computePosition().then(...)flushSync(setData)). Per the HTML spec, microtasks run before requestAnimationFrame, so as long as update() is called inside a layout effect, the position settles before transitionStatus is cleared and no transition fires.

In React 17, update() is not called inside the first layout effect. The floatingElement has to cross from PopoverStore to FloatingRootStore and then reach useFloating. Base UI switches its store implementation by React version: React 17 goes through the use-sync-external-store/shim polyfill, which attaches its store subscription in useEffect (runs after paint) rather than useLayoutEffect. As a result, the FloatingRootStore.floatingElement update that happens during the first commit cycle is not propagated to useFloating in the same cycle — the subscription isn't installed yet. useFloating only finds out in useEffect, which runs after the rAF that clears transitionStatus has already fired. By the time computePosition resolves and the position changes from translate(0,0) to translate(x,y), the inline transition: none has already been removed, so the CSS transition fires and the popover slides in from the top-left corner.

React 19 is unaffected because the native useSyncExternalStore has tearing protection — store changes during render are detected immediately, so update() is called inside the first layout effect, the microtask runs before rAF, and the position settles while transition: none is still applied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: popoverChanges related to the popover component.type: bugIt doesn't behave as expected.

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions