Anchor Positioning and Focus Order for Overlays
Permalink to "Anchor Positioning and Focus Order for Overlays"Overlay libraries traditionally render menus, popovers and tooltips in a “portal” at the end of body, so they escape overflow: hidden and stacking contexts. Visually the menu appears next to its button; in the DOM it is thousands of nodes away. For keyboard users, Tab from the menu jumps to the end of the page. For screen reader users browsing in reading order, the menu’s content appears after the footer, detached from the control that opened it. CSS anchor positioning and the top layer now let overlays stay next to their trigger in the DOM while rendering above everything.
This guide belongs to tooltips, popovers & overlays in data UIs.
Spec reference
Permalink to "Spec reference"- SC 2.4.3 Focus Order (A) — focusable components receive focus in an order that preserves meaning; an overlay whose Tab order continues at the end of the page fails this in practice.
- SC 1.3.2 Meaningful Sequence (A) — reading order should match the visual relationship between trigger and overlay.
- CSS Anchor Positioning —
anchor-nameon the trigger andposition-anchor/position-area/anchor()on the overlay; available in Chromium and, more recently, other engines (check support for your audience). - Top layer — elements shown with the Popover API or
showModal()render above all content regardless of their DOM position, removing the original reason for portals.
When to use — and when not to
Permalink to "When to use — and when not to"Prefer in-place DOM placement with the top layer (Popover API) and anchor positioning for new overlays: menus, filter panels, date pickers, previews. It gives you correct reading and focus order with no extra code.
Portals remain reasonable for modal dialogs (which make the rest of the page inert, so order matters less) and for legacy browser support. When you must portal a non-modal overlay, manage focus explicitly: move it in on open, return it to the trigger on close, and make Tab from the last control return to the element after the trigger.
The misapplication: a portalled row-actions menu in a grid. Focus moves into the menu, the user Tabs past the last item, and lands in the page footer — 300 rows away from where they were working.
Annotated code example
Permalink to "Annotated code example"<td>
<!-- Trigger and overlay are DOM siblings: reading and focus order stay local (SC 1.3.2, 2.4.3) -->
<button type="button" class="row-actions" popovertarget="menu-41" style="anchor-name: --row-41">
Actions for INV-1041
</button>
<div id="menu-41" popover="auto" role="menu" aria-label="Actions for INV-1041"
style="position-anchor: --row-41">
<button role="menuitem" type="button">Open</button>
<button role="menuitem" type="button">Duplicate</button>
<button role="menuitem" type="button">Archive</button>
</div>
</td>
[popover][role="menu"] {
margin: 0;
position-area: block-end span-inline-start; /* below the trigger, aligned to its end */
position-try-fallbacks: flip-block, flip-inline; /* stay on screen near edges */
}
// Fallback when the overlay must be portalled: restore order manually
menu.addEventListener('keydown', (e) => {
if (e.key === 'Tab' && !e.shiftKey && e.target === lastItem(menu)) {
e.preventDefault(); menu.hidePopover();
nextFocusableAfter(trigger)?.focus(); // continue where the trigger is (SC 2.4.3)
}
});
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Anchored overlay | Portalled overlay (unmanaged) |
|---|---|---|
| Open with Enter | Focus to first item | Focus to first item |
| Tab from last item | Next control after trigger | Next control after the portal — often page end |
| Shift+Tab from first item | Back to trigger | Previous element before the portal |
| Browse mode reading | Overlay content read after trigger | Overlay read at end of page |
| Escape | Close; focus to trigger | Close; focus to trigger if managed |
Integration context
Permalink to "Integration context"The Popover API that provides the top layer is covered in the Popover API for data grid overlays. Menus themselves follow row action menus with the menu button pattern. Overlays that must not be covered by sticky headers relate to focus not obscured by sticky headers.
Gotchas
Permalink to "Gotchas"Menu items and Tab. In a proper role="menu", Tab usually closes the menu and moves on rather than walking through items — arrow keys move between items. DOM placement still matters because “moves on” means “to the next element after the menu”.
Overlays inside scroll containers. Without the top layer, an absolutely positioned overlay inside an overflow: auto table container gets clipped. That was the original reason for portals; the top layer removes it.
Responsive layouts. On small screens, an anchored popover may become a bottom sheet. Keep it in place in the DOM; only its visual position changes.
Framework notes
Permalink to "Framework notes"Radix and Headless UI portal overlays by default; Radix’s Portal can be omitted (render content inline) when you use top-layer techniques, and its focus management handles Tab out of menus. Floating UI works with either; its FloatingFocusManager restores logical order for portalled content. Angular CDK Overlay renders into an overlay container at the end of body — use its focus utilities, or consider the Popover API for simple menus.
// Radix: render menu content inline instead of in a portal
<DropdownMenu.Root>
<DropdownMenu.Trigger>Actions for INV-1041</DropdownMenu.Trigger>
<DropdownMenu.Content>…</DropdownMenu.Content> {/* no <DropdownMenu.Portal> wrapper */}
</DropdownMenu.Root>
Worked example
Permalink to "Worked example"A grid’s column menus were portalled to the end of the document. A keyboard user opening the “Amount” column menu and pressing Tab twice found herself in the site footer, and had to Shift+Tab through dozens of links to get back. Switching the menus to popover="auto" siblings of their header buttons, positioned with anchor positioning (and a JavaScript fallback for browsers without it), restored natural order; Tab out of a menu now lands on the next column header.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Default overlay components to in-place rendering with the top layer, with portalling as an opt-in for legacy cases that also enables managed focus. Document anchor positioning support and the fallback strategy so teams know what they get in each browser.
FAQ
Permalink to "FAQ"Are portals inaccessible?
Not inherently, but they separate DOM order from visual order, which you then must repair with focus management. In-place rendering avoids the repair.
Does anchor positioning affect accessibility directly?
No — it is a layout feature. Its value is that it lets you keep overlays in the right DOM place while positioning them correctly.
What about tooltips?
Tooltips are not focusable, so focus order is unaffected, but reading order still benefits from placing them next to their trigger.
Do modal dialogs need to be in place?
Less so: a modal makes the rest of the page inert, so order outside it does not matter while it is open. Return focus correctly on close.
How do I test DOM order quickly?
Open the overlay, then use a screen reader’s browse mode to read forward from the trigger; the overlay content should come next. Or inspect the DOM in DevTools.
Related
Permalink to "Related"- Popover API for overlays — the top layer
- Row action menus — menu semantics
- Restoring focus after closing complex modals — the modal counterpart