Skip to content

docs: add RFC for headless positioning strategy - #36807

Open
Victor Genaev (mainframev) wants to merge 13 commits into
microsoft:masterfrom
mainframev:rfc/headless-positioning-strategy
Open

Victor Genaev (mainframev) wants to merge 13 commits into
microsoft:masterfrom
mainframev:rfc/headless-positioning-strategy

Conversation

@mainframev

@mainframev Victor Genaev (mainframev) commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

RFC for how headless Popover / Menu / Tooltip should close the gap between CSS anchor positioning (partial positioning API, not in every browser) and a JavaScript engine.

Compares four options and recommends Option A: land the explicit engine / PositioningEngineProvider from #36800. CSS stays the default; consumers opt into floating-ui (or another engine) where they need unsupported browsers or geometry-dependent options. Automatic fallback — lazy (#36124) or always-bundled — can still be added later as a provider on that contract.

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

📊 Bundle size report

✅ No changes found

@github-actions

Copy link
Copy Markdown

Pull request demo site: URL

- Firefox 147 ships CSS anchor positioning (Baseline 2026); CSS path needs
  position-area, i.e. Chromium 129+, so detect position-area not anchor-name
- Call out that virtual targets need a JS engine and are silently ignored
  on the CSS path
- Use value= on PositioningEngineProvider to match microsoft#36800
- Rule out library-level lazy loading; propose an opt-in auto engine on
  top of A's contract so fallback bundling stays in userland
- Note the oddbird polyfill's limits for Option D

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot removed the Type: RFC Request for Feedback label Oct 2, 2026
Victor Genaev (mainframev) added a commit to mainframev/fluentui that referenced this pull request Oct 2, 2026
Wraps an engine so surfaces keep CSS anchor positioning when the browser
supports position-area and their options do not need an engine, and hand
over to the engine otherwise. Implements the proposal from the
positioning RFC (microsoft#36807).

@Hotell Martin Hochel (Hotell) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left more comments ty

@mainframev

Victor Genaev (mainframev) commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor Author

UPDATE:

The RFC now covers three more options, based on the exploration PRs from Dmytro Kirpa (@dmytrokirpa). The existing options A–D and the proposal are unchanged.

  • Option E: CSS-semantics JavaScript fallback inside headless (feat(react-headless-components-preview): JavaScript fallback and plugins for usePositioning (exploration) #36841). The headless usePositioning keeps CSS anchor positioning as the primary path and ships its own small JS fallback that follows the CSS rules, used when the browser lacks anchor support, the target is virtual, or an option needs geometry. Internal plugins add autoSize, boundaries, arrowPadding, function offset, onPositioningEnd and the hidden state on both paths. No floating-ui; the hook grows from about 6 kB to 16 kB minified, always bundled. Behaviour is close to v9 but not pixel identical.
  • Option F: inlined, synchronous Floating UI port in react-positioning (chore(react-positioning): inline a trimmed, synchronous port of Floating UI (exploration) #36839). Replaces the @floating-ui/dom dependency with a trimmed port that Fluent owns. usePositioning drops from 29.2 kB to 24.5 kB minified, positions synchronously, and a parity spec shows identical results to the current engine. By itself it is not a headless strategy. It lowers the cost of the engine wherever it ships.
  • Option G: modular engine with plugins (feat(react-positioning): modular usePositioningCore() with positioning plugins (exploration) #36840, builds on F). Adds @fluentui/react-positioning/modular with usePositioningCore(options, plugins), so an engine consumer pays only for the plugins it passes: 12.6 kB bare, 21.3 kB with offset, flip, shift and arrow, 25.2 kB with everything. Plugins and order slots become public API.

@mainframev Victor Genaev (mainframev) added the Type: RFC Request for Feedback label Oct 7, 2026

@Hotell Martin Hochel (Hotell) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Option E strikes the best balance among all alternatives.

However, we need to evaluate whether saving 10 kB justifies the investment, given the added complexity and risk. That said, relying on native browser features to naturally reduce JavaScript overhead over time remains our long-term north star.

Areas for Improvement:

  • The RFC became implementation emit that is hard to follow
  • more human readable prompt tweak to generate output would be great

| `usePositioning` with the inlined port from [#36839](https://github.com/microsoft/fluentui/pull/36839) | 24.5 kB | 9.0 kB |
| `usePositioningCore` from [#36840](https://github.com/microsoft/fluentui/pull/36840), no plugins | 12.6 kB | 4.9 kB |
| `usePositioningCore` + offset, flip, shift, arrow plugins | 21.3 kB | 8.0 kB |
| headless `usePositioning` today → with the fallback and plugins from [#36841](https://github.com/microsoft/fluentui/pull/36841) | 6.05 kB → 15.9 kB | not reported |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come gzip is not reported ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's available #36841 (comment), and the numbers in the RFC are stale

Image

@dmytrokirpa Dmytro Kirpa (dmytrokirpa) Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so headless usePositioning went from 6kb (only css-anchor positioning with some features missing) to 12kb (css-anchor positioning by default for available scenarios and js-based fallback for cases when CSS anchoring is not availalbe or when it doesn't support some features like custom boundaries or autosize)


The exploration PRs change those numbers. The first three rows below come from the monosize bot on the PRs (same fixture setup as above); the last row is from the description of #36841, measured with an esbuild proxy and without gzip, so it is only indicative next to the fixture rows.

| What | Minified | Gzip |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be helpful to have this aligned with number of proposal, its hard to follow taking into consideration the lenght and complexity of this doc


Internal plugins handle the options CSS cannot express. A plugin can declare that an option forces the JavaScript path (`requiresJs`), adjust the input of the fallback (`prepare`), and run after every update on both paths (`apply`): `offsetPlugin` (function `offset`), `boundaryPlugin` (`flipBoundary`, `overflowBoundary`, `overflowBoundaryPadding`), `autoSizePlugin`, `arrowPlugin` (`arrowPadding`, `arrowRef`) and `hidePlugin` (`data-positioning-hidden`, `data-positioning-escaped`, `onPositioningEnd`). Because `apply` also runs on the CSS path, `autoSize`, the arrow and the hidden state work in browsers with anchor support too. The plugin list is a module constant, not consumer API. One update loop (capturing `scroll`, `resize`, a `ResizeObserver`, debounced) serves both paths and replaces the placement observer; on the CSS path the applied placement is detected from the rects and written to `data-placement`.

The headless package stops keeping its own narrower copy of the positioning types: `PositioningProps`, `PositioningShorthand`, `Position`, `Alignment`, `Offset`, `PositioningBoundary`, `PositioningVirtualElement` and the rest are re-exports from `@fluentui/react-positioning`, with a compile-time test that fails if they diverge. `useTransform` and `shiftToCoverTarget` are accepted but have no effect. Fallback and plugins are always bundled: `usePositioning` grows from 6.05 kB to 15.9 kB minified (about 5 kB for the fallback and 4.5 kB for the plugins).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in v9 + headless scenario on 1 page user will pay for both floating ui and our 15kb correct ? can this be ported to v9 as basically the feature parity should be identical ( with mentioned pixel diffs - would be good to elaborate on these pixel diffs more as ATM it's ambiguous and not human understandable what it means in sense of user impact )

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's partially correct, floating-ui is ~30Kb (minified, with all modules/middlewares we use). And headless usePositioning currently adds ~6Kb on top if used alongside, and that number will grow to ~12kb on top floating ui as that code is not shared.

We could explore the option of switching react-positioning under the hood from @floating-ui to a custom solution, since floatingUI is not exposed anywhere and it won't be a BC from API contract point of view. But we definitely need a better test coverage for behavior parity and some perf benchmarks


- Bundle: every headless page pays the fallback and the plugins (6 kB → 16 kB minified), including pages that only ever use CSS-eligible options in browsers with anchor support. Smaller than C, but the same shape of cost.
- Maintenance: a third positioning implementation in the repository (CSS, a CSS-mimicking engine, floating-ui in v9), with its own containing-block, clipping and geometry code. CSS anchor semantics are still moving, so the mimic has to track browser changes: Chrome 154 keeps boxes that overflow the viewport inside it, Chromium 141 does not, and the fallback follows 154 (the comparison spec skips itself in Electron 130).
- Parity with v9: behaviour is "close to, not pixel identical with" the floating-ui-based `react-positioning`. A v9 → headless migration can move surfaces by a few pixels, and an app that depends on the exact v9 placement has no way to get it. `shiftToCoverTarget` and `useTransform` are silently no-ops.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perf wise, is our custom "fork" taking bits directly from floating ui for the fallbacks ? im concerned about perf regressions in user land.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants