Skip to content

feat(headless & react-positioning): add engine option to allow floating-ui based positioning in headless - #36800

Draft
Victor Genaev (mainframev) wants to merge 8 commits into
microsoft:masterfrom
mainframev:feat/headless-positioning-engine-factory
Draft

Victor Genaev (mainframev) wants to merge 8 commits into
microsoft:masterfrom
mainframev:feat/headless-positioning-engine-factory

Conversation

@mainframev

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

Copy link
Copy Markdown
Contributor

Previous Behavior

Headless surfaces are positioned with CSS anchor positioning only. Options that have no CSS equivalent (autoSize, flipBoundary, overflowBoundary, overflowBoundaryPadding, shiftToCoverTarget, arrowPadding, useTransform, onPositioningEnd) are not available.

New Behavior

Usage

Per surface, alongside any other positioning option:

import { Menu } from '@fluentui/react-headless-components-preview/menu';
import { floatingUIPositioningEngine } from '@fluentui/react-positioning';

<Menu positioning={{ position: 'below', align: 'start', autoSize: true, engine: floatingUIPositioningEngine }}>
  ...
</Menu>

App-wide via context; every headless surface below it uses the engine, withArrow is positioned by it as well:

import { PositioningEngineProvider } from '@fluentui/react-headless-components-preview/positioning';

<PositioningEngineProvider value={floatingUIPositioningEngine}>
  <Popover withArrow positioning={{ position: 'after', align: 'center', offset: 10 }}>
    ...
  </Popover>
</PositioningEngineProvider>

Imperative updates and end-of-positioning events work the same way as in v9:

const positioningRef = React.useRef<PositioningImperativeRef>(null);

<Popover
  positioning={{
    positioningRef,
    shiftToCoverTarget: true,
    onPositioningEnd: event => console.log(event.detail.placement, event.detail.escaped),
    engine: floatingUIPositioningEngine,
  }}
/>;

positioningRef.current?.updatePosition();

Without an engine the same props still position via CSS anchors; engine-only options are ignored with a dev warning.

  • When an engine is set it replaces CSS anchor positioning for that surface entirely, so two positioners never mix. Component-level engine wins over the provider.
  • PositioningProps in headless is widened to the full canonical contract. Engine-only options without an engine log a dev warning and are ignored.
  • Headless defaults strategy to fixed (surfaces live in the top layer).
  • Arrow support: usePositioning returns arrowRef, wired into Popover/Tooltip.
  • The engine owns the top-layer reset (inset: auto) and keeps data-placement current with the logical placement, so placement-keyed CSS keeps working. This is documented as the PositioningEngine contract.
  • @floating-ui/* stays out of the headless bundle; verify-bundle-isolation forbids it.

Stories: Concepts/Positioning gets an Engine overview plus one story per engine-only option, mirroring the v9 positioning docs.

Impact on v9

None, the change is additive:

  • react-positioning public API only gains exports (createFloatingUIPositioningEngine, floatingUIPositioningEngine, PositioningEngine, PositioningEngineCreateParams, plus types v9 already used internally: PositioningOptions, PositionManager, OnPositioningEndEvent, OnPositioningEndEventDetail, PositioningPlacement). Nothing removed, renamed or retyped, hence a minor change file.
  • usePositioning / usePositioningOptions return the same shape. The middleware assembly was moved as-is into resolvePositioningOptions (same middleware, same order, same subset forwarded to PositioningConfigurationFn, same strategy fallback expression) and the per-field dependency list is unchanged.
  • createPositionManager, all middleware and the v9 component packages are untouched. The top-layer inset reset and data-placement writing only happen in createFloatingUIPositioningEngine.create(), which v9 never calls.
  • Existing react-positioning tests pass unchanged; the API report diff is additions only.
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-avatar
Avatar
0 B
0 B
49.059 kB
15.626 kB
🆕 New entry
react-avatar
AvatarGroup
0 B
0 B
17.523 kB
7.008 kB
🆕 New entry
react-avatar
AvatarGroupItem
0 B
0 B
62.043 kB
19.487 kB
🆕 New entry
react-breadcrumb
@fluentui/react-breadcrumb - package
0 B
0 B
104.662 kB
29.481 kB
🆕 New entry
react-charts
AreaChart
0 B
0 B
408.946 kB
127.757 kB
🆕 New entry
react-charts
DeclarativeChart
0 B
0 B
761.445 kB
223.402 kB
🆕 New entry
react-charts
DonutChart
0 B
0 B
319.403 kB
98.517 kB
🆕 New entry
react-charts
FunnelChart
0 B
0 B
310.818 kB
95.291 kB
🆕 New entry
react-charts
GanttChart
0 B
0 B
391.859 kB
122.174 kB
🆕 New entry
react-charts
GaugeChart
0 B
0 B
319.03 kB
97.959 kB
🆕 New entry
react-charts
GroupedVerticalBarChart
0 B
0 B
399.964 kB
124.946 kB
🆕 New entry
react-charts
HeatMapChart
0 B
0 B
394.055 kB
123.249 kB
🆕 New entry
react-charts
HorizontalBarChart
0 B
0 B
299.081 kB
90.963 kB
🆕 New entry
react-charts
HorizontalBarChartWithAxis
0 B
0 B
63 B
83 B
🆕 New entry
react-charts
Legends
0 B
0 B
238.238 kB
71.519 kB
🆕 New entry
react-charts
LineChart
0 B
0 B
420.234 kB
130.809 kB
🆕 New entry
react-charts
PolarChart
0 B
0 B
347.255 kB
108.459 kB
🆕 New entry
react-charts
SankeyChart
0 B
0 B
215.08 kB
69.057 kB
🆕 New entry
react-charts
ScatterChart
0 B
0 B
399.411 kB
124.83 kB
🆕 New entry
react-charts
Sparkline
0 B
0 B
81.156 kB
26.99 kB
🆕 New entry
react-charts
VerticalBarChart
0 B
0 B
436.377 kB
129.853 kB
🆕 New entry
react-charts
VerticalStackedBarChart
0 B
0 B
405.963 kB
126.318 kB
🆕 New entry
react-checkbox
Checkbox
0 B
0 B
31.749 kB
11.322 kB
🆕 New entry
react-combobox
Combobox (including child components)
0 B
0 B
141.16 kB
45.659 kB
🆕 New entry
react-combobox
Dropdown (including child components)
0 B
0 B
140.959 kB
45.431 kB
🆕 New entry
react-components
react-components: all base hooks
0 B
0 B
218.87 kB
68.561 kB
🆕 New entry
react-components
react-components: Button, FluentProvider & webLightTheme
0 B
0 B
67.731 kB
19.588 kB
🆕 New entry
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
0 B
0 B
228.786 kB
68.891 kB
🆕 New entry
react-components
react-components: FluentProvider & webLightTheme
0 B
0 B
40.961 kB
13.674 kB
🆕 New entry
react-components
react-components: entire library
0 B
0 B
1.286 MB
323.227 kB
🆕 New entry
react-datepicker-compat
DatePicker Compat
0 B
0 B
219.267 kB
65.007 kB
🆕 New entry
react-dialog
Dialog (including children components)
0 B
0 B
91.95 kB
28.631 kB
🆕 New entry
react-field
Field
0 B
0 B
23.226 kB
8.656 kB
🆕 New entry
react-headless-components-preview
react-headless-components-preview: entire library
0 B
0 B
244.959 kB
69.039 kB
🆕 New entry
react-headless-components-preview
@fluentui/react-headless-components-preview/tag-picker
0 B
0 B
56.823 kB
18.511 kB
🆕 New entry
react-headless-components-preview
@fluentui/react-headless-components-preview/teaching-popover
0 B
0 B
38.86 kB
12.758 kB
🆕 New entry
react-input
Input
0 B
0 B
26.495 kB
8.759 kB
🆕 New entry
react-list
List
0 B
0 B
75.802 kB
23.526 kB
🆕 New entry
react-list
ListItem
0 B
0 B
100.029 kB
30.673 kB
🆕 New entry
react-menu
Menu (including children components)
0 B
0 B
162.434 kB
51.594 kB
🆕 New entry
react-menu
Menu (including selectable components)
0 B
0 B
165.599 kB
52.196 kB
🆕 New entry
react-overflow
hooks only
0 B
0 B
10.203 kB
3.694 kB
🆕 New entry
react-persona
Persona
0 B
0 B
56.012 kB
17.572 kB
🆕 New entry
react-popover
Popover
0 B
0 B
127.744 kB
41.192 kB
🆕 New entry
react-portal-compat
PortalCompatProvider
0 B
0 B
5.341 kB
2.146 kB
🆕 New entry
react-positioning
usePositioning
0 B
0 B
29.863 kB
10.356 kB
🆕 New entry
react-positioning
useSafeZoneArea
0 B
0 B
13.461 kB
5.425 kB
🆕 New entry
react-progress
ProgressBar
0 B
0 B
20.474 kB
7.96 kB
🆕 New entry
react-radio
Radio
0 B
0 B
28.419 kB
9.273 kB
🆕 New entry
react-radio
RadioGroup
0 B
0 B
14.186 kB
5.731 kB
🆕 New entry
react-select
Select
0 B
0 B
27.085 kB
9.812 kB
🆕 New entry
react-slider
Slider
0 B
0 B
33.662 kB
11.627 kB
🆕 New entry
react-spinbutton
SpinButton
0 B
0 B
34.635 kB
11.423 kB
🆕 New entry
react-swatch-picker
@fluentui/react-swatch-picker - package
0 B
0 B
94.156 kB
27.991 kB
🆕 New entry
react-switch
Switch
0 B
0 B
34.418 kB
10.971 kB
🆕 New entry
react-table
DataGrid
0 B
0 B
148.754 kB
44.382 kB
🆕 New entry
react-table
Table (Primitives only)
0 B
0 B
38.994 kB
13.096 kB
🆕 New entry
react-table
Table as DataGrid
0 B
0 B
120.628 kB
34.158 kB
🆕 New entry
react-table
Table (Selection only)
0 B
0 B
67.5 kB
19.347 kB
🆕 New entry
react-table
Table (Sort only)
0 B
0 B
66.163 kB
18.974 kB
🆕 New entry
react-tag-picker
@fluentui/react-tag-picker - package
0 B
0 B
176.621 kB
55.113 kB
🆕 New entry
react-tags
InteractionTag
0 B
0 B
13.882 kB
5.508 kB
🆕 New entry
react-tags
Tag
0 B
0 B
30.51 kB
9.744 kB
🆕 New entry
react-tags
TagGroup
0 B
0 B
71.063 kB
21.924 kB
🆕 New entry
react-teaching-popover
TeachingPopover
0 B
0 B
102.648 kB
32.394 kB
🆕 New entry
react-textarea
Textarea
0 B
0 B
24.86 kB
9.032 kB
🆕 New entry
react-timepicker-compat
TimePicker
0 B
0 B
143.879 kB
47.04 kB
🆕 New entry
react-tooltip
Tooltip
0 B
0 B
55.845 kB
19.762 kB
🆕 New entry
react-tree
FlatTree
0 B
0 B
137.661 kB
41.214 kB
🆕 New entry
react-tree
PersonaFlatTree
0 B
0 B
139.524 kB
41.737 kB
🆕 New entry
react-tree
PersonaTree
0 B
0 B
135.605 kB
40.514 kB
🆕 New entry
react-tree
Tree
0 B
0 B
133.748 kB
40.011 kB
🆕 New entry

🤖 This report was generated against

@github-actions

Copy link
Copy Markdown

Pull request demo site: URL

@@ -19,9 +19,11 @@ experience to provide. Fluent UI Headless leaves that tradeoff to the consuming
anchor-positioning polyfill. This keeps the common path lean and requires little application code,

@github-actions github-actions Bot Sep 25, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/Avatar Converged 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Avatar Converged.size+icon+badge+square.normal.chromium.png 1 Changed
vr-tests-react-components/Avatar Converged.badgeMask.normal.chromium.png 3 Changed
vr-tests-react-components/Drawer 6 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Drawer.inline drawer separator position start - Dark Mode.chromium.png 2 Changed
vr-tests-react-components/Drawer.inline drawer separator position start - High Contrast.chromium.png 3 Changed
vr-tests-react-components/Drawer.inline drawer.chromium.png 2 Changed
vr-tests-react-components/Drawer.overlay drawer position start - Dark Mode.chromium.png 2 Changed
vr-tests-react-components/Drawer.overlay drawer position start - High Contrast.chromium.png 3 Changed
vr-tests-react-components/Drawer.overlay drawer.chromium.png 2 Changed
vr-tests-react-components/Field 10 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Field.Checkbox.default.chromium.png 8 Changed
vr-tests-react-components/Field.ProgressBar.default.chromium.png 16 Changed
vr-tests-react-components/Field.Slider.default.chromium.png 8 Changed
vr-tests-react-components/Field.RadioGroup.default.chromium.png 8 Changed
vr-tests-react-components/Field.SpinButton.default.chromium.png 8 Changed
vr-tests-react-components/Field.Switch.default.chromium.png 8 Changed
vr-tests-react-components/Field.horizontal+longLabel.default.chromium.png 8 Changed
vr-tests-react-components/Field.validation-error.default.chromium.png 8 Changed
vr-tests-react-components/Field.Textarea.default.chromium.png 8 Changed
vr-tests-react-components/Field.validation-warning.default.chromium.png 8 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 413 Changed
vr-tests-react-components/MessageBar 6 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/MessageBar.Intents - Dark Mode.default.chromium.png 36 Changed
vr-tests-react-components/MessageBar.Intents - High Contrast.default.chromium.png 79 Changed
vr-tests-react-components/MessageBar.Intents.default.chromium.png 51 Changed
vr-tests-react-components/MessageBar.Multiline Without Actions.default.chromium.png 51 Changed
vr-tests-react-components/MessageBar.Multiline.default.chromium.png 51 Changed
vr-tests-react-components/MessageBar.Multiline No Actions.default.chromium.png 51 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 609 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 138 Changed
vr-tests-react-components/ProgressBar converged 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 84 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 41 Changed
vr-tests-react-components/SearchBox Converged 6 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/SearchBox Converged.Appearance- filled-lighter.hover.chromium.png 33 Changed
vr-tests-react-components/SearchBox Converged.Disabled, appearance- filled-lighter.hover.chromium.png 11 Changed
vr-tests-react-components/SearchBox Converged.Disabled, appearance- underline.focused.chromium.png 13 Changed
vr-tests-react-components/SearchBox Converged.Size- large.hover.chromium.png 45 Changed
vr-tests-react-components/SearchBox Converged.Size- small.hover.chromium.png 18 Changed
vr-tests-react-components/SearchBox Converged.With value.hover.chromium.png 33 Changed
vr-tests-react-components/SwatchPicker Converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/SwatchPicker Converged.size.default.chromium.png 2 Changed
vr-tests-react-components/SwatchPicker Converged.spacing.default.chromium.png 2 Changed
vr-tests-react-components/SwatchPicker Converged.shape.default.chromium.png 2 Changed
vr-tests-react-components/TagPicker 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.chromium.png 658 Changed
vr-tests-react-components/Toast 6 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Toast.Full Toast Inverted - High Contrast.Toast visible.chromium.png 54 Changed
vr-tests-react-components/Toast.Full Toast.Toast visible.chromium.png 36 Changed
vr-tests-react-components/Toast.Title Only.Toast visible.chromium.png 36 Changed
vr-tests-react-components/Toast.Title Only Inverted - High Contrast.Toast visible.chromium.png 54 Changed
vr-tests-react-components/Toast.Without Subtitle Inverted.Toast visible.chromium.png 25 Changed
vr-tests-react-components/Toast.Without Subtitle Inverted - High Contrast.Toast visible.chromium.png 54 Changed
vr-tests-react-components/Toolbar Converged 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Toolbar Converged.Large.default.chromium.png 5 Changed
vr-tests-react-components/Toolbar Converged.Small.default.chromium.png 5 Changed
vr-tests-react-components/Toolbar Converged.Transparent.default.chromium.png 5 Changed
vr-tests-react-components/Toolbar Converged.Vertical.Button Pressed.chromium.png 5 Changed
vr-tests-react-components/Tree 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Tree.layout.default.chromium.png 115 Changed

There were 149 duplicate changes discarded. Check the build logs for more information.

…SS.supports

The React 17 integration run uses Cypress 13 / Electron 118, which predates CSS anchor
positioning, so position-anchor is dropped and the observer reports the centred placement.
@dmytrokirpa

Copy link
Copy Markdown
Contributor

I’d suggest documenting the positioning strategy in an RFC before we settle on the API. There are several reasonable approaches with different trade-offs:

  • Explicit engine selection: consumers pass an engine via positioning or a provider/context; predictable and opt-in, but requires consumer configuration.

  • Automatic fallback: use CSS anchor positioning when it supports the requested behavior, and fall back to a JavaScript engine when browser support or options require it; simpler for consumers, but adds detection, loading, and first-paint considerations.

  • Extend CSS-based positioning: support more cases with CSS anchor features and custom logic, while recognizing that geometry-dependent behavior may effectively become another positioning engine.

  • Limit the API: keep CSS-based positioning and explicitly drop options such as custom boundaries to make them consumer's concern.

The RFC would benefit from comparing these options based on bundle impact, browser behavior, API complexity, parity with react-positioning, and maintenance cost. It should then recommend a default and clarify which options are supported on each path. My initial preference is automatic fallback for the default experience, with an explicit override for consumers who need control, but I want to avoid biasing the RFC 😄

@mainframev

Copy link
Copy Markdown
Contributor Author

I’d suggest documenting the positioning strategy in an RFC before we settle on the API. There are several reasonable approaches with different trade-offs:

  • Explicit engine selection: consumers pass an engine via positioning or a provider/context; predictable and opt-in, but requires consumer configuration.
  • Automatic fallback: use CSS anchor positioning when it supports the requested behavior, and fall back to a JavaScript engine when browser support or options require it; simpler for consumers, but adds detection, loading, and first-paint considerations.
  • Extend CSS-based positioning: support more cases with CSS anchor features and custom logic, while recognizing that geometry-dependent behavior may effectively become another positioning engine.
  • Limit the API: keep CSS-based positioning and explicitly drop options such as custom boundaries to make them consumer's concern.

The RFC would benefit from comparing these options based on bundle impact, browser behavior, API complexity, parity with react-positioning, and maintenance cost. It should then recommend a default and clarify which options are supported on each path. My initial preference is automatic fallback for the default experience, with an explicit override for consumers who need control, but I want to avoid biasing the RFC 😄

Agree an RFC is the right place to settle this, happy to write it up with those four options 👍🏻

I'm a bit sceptical about the automatic approach: it moves detection and floating-ui loading into the library and has a first-paint cost (unpositioned surface or a jump until the fallback runs). But we clearly have different favourites here, so let's compare them properly in the RFC and decide there

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