AnchoredOverlay: Add support for CSS anchor positioning#7604
AnchoredOverlay: Add support for CSS anchor positioning#7604
Conversation
🦋 Changeset detectedLatest commit: 452d29e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
|
🤖 Lint and formatting issues have been automatically fixed and committed to this PR. |
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
|
🤖 Lint and formatting issues have been automatically fixed and committed to this PR. |
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
|
🤖 Lint issues have been automatically fixed and committed to this PR. |
There was a problem hiding this comment.
Pull request overview
This PR adds CSS Anchor Positioning support to AnchoredOverlay, gated behind the primer_react_css_anchor_positioning feature flag. When enabled, the overlay uses native CSS anchor positioning (with a polyfill for unsupported browsers) instead of JS-based positioning via a portal. This allows overlays within dialogs, sticky elements, and scroll containers to position correctly. The @oddbird/css-anchor-positioning polyfill is added as a runtime dependency.
Changes:
- CSS Anchor Positioning feature flag and core logic added to
AnchoredOverlayandOverlaycomponents ActionMenuupdated to properly mergeclassNamefrom anchor props with user-provided class names- New Storybook stories demonstrating anchor positioning in various layouts (grids, dialogs, scroll containers, sticky headers)
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx |
Core logic: adds CSS anchor positioning path, polyfill invocation, Wrapper div, anchor/overlay class application |
packages/react/src/AnchoredOverlay/AnchoredOverlay.module.css |
New CSS classes: .Wrapper (anchor-scope), .Anchor (anchor-name), .AnchoredOverlay (anchor position + side-specific layout rules) |
packages/react/src/Overlay/Overlay.tsx |
Skips portal when flag is on; sets data-anchor-position attribute for all Overlay instances |
packages/react/src/Overlay/Overlay.module.css |
Gates top/left/right/bottom CSS var positioning on [data-anchor-position='false'] |
packages/react/src/FeatureFlags/DefaultFeatureFlags.ts |
Adds primer_react_css_anchor_positioning flag (default: false) |
packages/react/src/ActionMenu/ActionMenu.tsx |
Merges anchor className (including classes.Anchor) with user-provided classNames for all anchor path types |
packages/react/src/ActionMenu/ActionMenu.test.tsx |
Tests verify classes.Anchor is applied to anchor/button elements |
packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx |
New stories demonstrating CSS anchor positioning in various layout scenarios |
packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.module.css |
New CSS for story layouts |
packages/react/src/ActionMenu/ActionMenu.examples.stories.tsx |
New stories for centered ActionMenu and two-menu scenario |
packages/react/package.json |
Adds @oddbird/css-anchor-positioning as a runtime dependency |
package-lock.json |
Lock file updates for new dependencies |
.changeset/nine-buttons-lose.md |
Minor release changeset for the feature |
Comments suppressed due to low confidence (1)
packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx:241
- When
cssAnchorPositioningis enabled, the overlay becomes immediately visible (visibility={cssAnchorPositioning || position ? 'visible' : 'hidden'}), butuseFocusTrapanduseFocusZoneare still gated on!open || !position. This means on the first render whenpositionhasn't been calculated yet, the overlay is visible but the focus trap is not active. This can cause an accessibility gap where keyboard users can navigate out of the overlay before the focus trap activates.
The disabled condition for useFocusTrap and useFocusZone should be updated to account for CSS anchor positioning: when cssAnchorPositioning is true, the focus should be trapped as soon as the overlay is open (disabled: !open), rather than waiting for the JS-calculated position.
useFocusZone({
containerRef: overlayRef,
disabled: !open || !position,
...focusZoneSettings,
})
useFocusTrap({containerRef: overlayRef, disabled: !open || !position, ...focusTrapSettings})
packages/react/src/AnchoredOverlay/AnchoredOverlay.features.stories.tsx
Outdated
Show resolved
Hide resolved
| import {type ResponsiveValue} from '../hooks/useResponsiveValue' | ||
| import {IconButton, type IconButtonProps} from '../Button' | ||
| import {XIcon} from '@primer/octicons-react' | ||
| import polyfill from '@oddbird/css-anchor-positioning/fn' |
There was a problem hiding this comment.
Does this mean the polyfill is already part of the bundle?
There was a problem hiding this comment.
Yeah! We had some of the polyfill's dependencies already (i.e., floating-ui), which is good since we're already accounting for those in our current bundle size.
There was a problem hiding this comment.
Oh that's interesting. I realised we no longer have a comment from the size action. Would be good to have a number before we merge.
It does run but not sure what the results are 😅
https://github.com/primer/react/actions/runs/22721918648/job/65886127997?pr=7604
There was a problem hiding this comment.
Having Copilot take a stab at this here: #7628. Looks like we deleted the size.yml which added the comment. This adds it back 🤔
| }, [open, overlayRef, updateOverlayRef]) | ||
|
|
||
| if (cssAnchorPositioning && open) { | ||
| applyAnchorPositioningPolyfill() |
There was a problem hiding this comment.
Should we wait for open or load it already if we know we will need it?
There was a problem hiding this comment.
Maybe just load? 🤔 I'll go ahead and remove the open from the conditional!
| bottom: var(--bottom, auto); | ||
| } | ||
|
|
||
| &[data-anchor-position='false']:not([data-responsive='fullscreen']) { |
There was a problem hiding this comment.
bonus bugfix hiding in this PR?
There was a problem hiding this comment.
Hah, yeah - we explicitly set the top and left to 0 when fullscreen is used, so without this it would get overridden.
siddharthkp
left a comment
There was a problem hiding this comment.
Very nice work!
The direction looks perfect, left a few clarifying questions
|
@siddharthkp - should be ready for another review! I addressed some feedback, and am working on the size report in #7628 😁 |
|
👋 Hi from github/github-ui! Your integration PR is ready: https://github.com/github/github-ui/pull/15261 |
|
Integration test results from github/github-ui:
CI check runs linting, type checking, and unit tests. Check the workflow logs for specific failures. Need help? If you believe this failure is unrelated to your changes, please reach out to the Primer team for assistance. |
Closes https://github.com/github/primer/issues/6446
The first batch of work to add CSS anchor positioning to overlay components.
Example of CSS anchor positioning:
Screen.Recording.2026-03-04.at.7.33.32.AM.mov
Video description
In the video, we show the `AnchoredOverlay` opening and being positioned directly under the trigger. We then show off the overlay repositioning when there's lack of space.
Changelog
New
AnchoredOverlay, gated by theprimer_react_css_anchor_positioningfeature flag.ActionMenuto ensure that the anchor button receives the correct anchor class for CSS anchor positioning, and improved merging of class names between anchor and button props.AnchoredOverlay.features.stories.tsxto demonstrate anchor positioning in various layouts, including centered overlays, grid layouts, within dialogs (including nested and overflowing dialogs), scroll containers, and sticky headers.Rollout strategy
Testing & Reviewing
Merge checklist