Skip to content

Flip anchored elements when their content grows after opening - #3905

Open
jdeff wants to merge 3 commits into
tailwindlabs:mainfrom
jdeff:fix/anchor-stale-max-height
Open

jdeff wants to merge 3 commits into
tailwindlabs:mainfrom
jdeff:fix/anchor-stale-max-height

Conversation

@jdeff

@jdeff jdeff commented Oct 14, 2026 •

Copy link
Copy Markdown

This PR fixes anchored elements staying on a cramped side when their content grows after they open, for example results that load after opening. The panel keeps the height it had while loading and scrolls, and when the content has its own scrolling list you get a scrollbar inside a scrollbar, even though the other side has room for all of it.

The size middleware caps the panel with an inline max-height that stays on the element. When the content grows, the panel can only grow up to that cap, so on the next placement flip measures a panel that fits and never moves it.

Floating UI treats this as the job of the code that writes the cap: size only reports availableHeight to apply. Its maintainer described this exact cause in floating-ui/floating-ui#2424 and floating-ui/floating-ui#2954, and the fix was the documented apply pattern (size docs): availableHeight >= elements.floating.scrollHeight ? '' : cap. We didn't use that pattern here because it loops on a common Headless UI layout. A panel whose flex child shrinks to the cap (a scrolling body with a footer) has a scrollHeight equal to the cap, so the >= removes the cap, the panel overflows, gets capped again, and repeats. It also replaces --anchor-max-height with ''.

With this PR, a middleware that runs before the others measures the panel under --anchor-max-height when the panel has reached its cap, and hands that height to flip and size as explicit rects (reset: { rects: { reference, floating } }). They place the panel by the height it can actually take:

  • It runs once per placement. size requests its own reset after capping the panel. Without the middlewareData check, the two would undo each other until Floating UI's reset limit, and the last flip would run on capped measurements.
  • It checks whether the panel has reached its cap, not whether it overflows. That also covers panels whose flex child shrinks to the cap (a scrolling body with a footer), which never overflow.
  • It keeps --anchor-max-height, so flip never judges a panel at a height it can't take.
  • It leaves nothing behind in the page. In one synchronous step it sets position: fixed, lifts the cap, reads the height, and puts the cap, the position and the scroll positions of the panel and anything scrolled inside it back. A first version left the panel uncapped and returned reset: { rects: true }, which let the full-height panel take part in page layout. Where scrollbars take up space (Windows, Linux, macOS set to always show them), a panel taller than the page added a page scrollbar, the viewport resize repositioned the panel, the cap removed the scrollbar again, and it looped, resetting the panel's scroll position each pass. Fixed positioning keeps the measurement out of the page's scrollable overflow.
  • Panels below their cap skip it after one computed-style read and one rect read, which is cheap because layout is already up to date when computePosition runs. A panel at its cap pays one extra layout per placement.
  • anchor="selection" is unchanged. It uses the inner middleware instead of flip.

On floating-ui/floating-ui#2424 the maintainer also noted that flipping when content changes can feel disorienting, and suggested fallbackStrategy: 'initialPlacement' for that. Headless UI already uses flip's default best-fit strategy, and autoUpdate already re-runs it whenever the panel resizes; it just measures the wrong height when it decides. This change doesn't make anything flip that the existing settings wouldn't. It means the decision uses the panel's real height, instead of squeezing the content into the loading state's room with a scrollbar inside a scrollbar.

Fixes #3904

Test plan

Added a playground example, popover/popover-growing-content: a popover 200px above the bottom of the viewport that opens with a loading state, then loads results into a list with its own max-h-72 scrolling, the same as the reproduction. Measured in Chromium at 1280×800:

Placement Panel Panel scrolls List scrolls
Before bottom start 131px yes yes
After top start 335px no yes

The existing test suite passes (1,068 passed, 30 skipped). jsdom doesn't do layout, so these tests can't cover this.

We've run the same middleware as a patch-package patch on 2.2.10, with browser tests for growing content, flex panels that shrink to the cap, the reset loop, scroll positions surviving a recompute, and a tall panel staying put with scrollbars that take up space (no style changes while idle).

The `size` middleware caps the panel with an inline `max-height` that
stays on the element. When the content grows after the panel was placed,
the panel can only grow up to that stale cap, so `flip` keeps measuring
a panel that fits and never moves it to the side with more room.

Add a middleware before the others that, when the panel has reached its
cap, lifts it back to `--anchor-max-height` and re-measures, once per
placement.

Fixes tailwindlabs#3904

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 14, 2026

Copy link
Copy Markdown

@jdeff is attempting to deploy a commit to the Tailwind Labs Team on Vercel.

A member of the Team first needs to authorize it.

jdeff and others added 2 commits October 14, 2026 13:43
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Leaving the panel uncapped and re-measuring it let the full-height panel
take part in page layout. Where scrollbars take up space, a panel taller
than the page added a page scrollbar, the viewport resize repositioned
the panel, the cap removed the scrollbar again, and it looped, resetting
the panel's scroll position on every pass.

Measure with the panel fixed-positioned, so it stays out of the page's
scrollable overflow, put the cap and scroll positions back in the same
step, and hand the measured height to `flip` and `size` as explicit
rects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Anchored panel doesn't flip when its content grows after opening (stale max-height from size)

1 participant