Positioning
Tooltip, Popover, and Dropdown's menu all place a floating element next to a trigger through the same engine. It prefers CSS anchor positioning where the browser supports it, and measures and places the element itself where it does not. Either way it resolves to a physical, on-screen placement: flipping at the viewport edge, escaping clipping ancestors, and reporting back exactly what it rendered.
One placement vocabulary
A placement is a side (top, bottom, left, right) with an optional -start/-end alignment. A bare
side centers on the trigger's cross axis. Tooltip and Popover take all eight placements as a placement prop. Dropdown's menu exposes
only align (start/center/end), because its side
is managed for you: it opens below the trigger and flips above only when there is not room. A
menu button's menu belongs on the block axis, not wherever a consumer might place it.
Logical-first, resolved through the trigger
start/end follow reading direction, the same rule the layout
primitives' logical axes follow. Where a
placement names a physical side directly (left/right) that also
resolves through direction: under dir="rtl", left renders on the
physical right. The direction that matters is the trigger's, not the floating
element's own. A tooltip or panel can be appended somewhere else in the document so it can sit
in the top layer, so its own inherited direction can differ from the trigger it is describing.
Reading the trigger's computed direction directly keeps placement correct wherever the floating
element itself lives.
The top layer
Open floating elements render in the top layer, a separate layer the browser
paints above the whole page. Nothing on the page can cover them: not an ancestor's overflow: hidden, not a stacking context, not any z-index, however
large. The top layer is not part of the z-index contest at all.
You never opt into this. Tooltip, Popover, and Dropdown use the top layer wherever the browser
supports it (Chrome and Edge 114+, Safari 17+, Firefox 125+, so every evergreen browser since
early 2024). In an older browser they fall back to ordinary position: fixed elements, and only there do the --hz-z-* tokens decide who
stacks above whom. Their tiers are ordered so that fallback matches what the top layer gives you for
free.
The practical rule: for these components, stacking is handled, so do not reach for z-index. Keep the --hz-z-* tokens for floating interfaces you build yourself
out of ordinary elements, like a sticky header or a custom overlay. They remain the page's one shared
stacking scale, and the top layer sits above all of it.
Automatic flip, live
The side and alignment you ask for are a request, not a promise. If there is not room on the
side you asked for, the floating element flips to the opposite side, and it keeps re-checking as
you scroll and resize for as long as it is open. The side that actually rendered is readable
back from data-side on the floating element, re-stamped from real geometry every time it
flips. data-align reports the alignment your logical request resolved to, which flipping never
changes. Flipping answers to the edges of your window, never to a scroll container's: a clipping frame
is escaped entirely by the top layer, not flipped around.
Flipping is the behavior both paths share, and it is deliberately the only one promised. Where
CSS anchor positioning is available, the browser does the work through position-try-fallbacks, which offers a flip and nothing else. The JavaScript
fallback, used where anchor positioning is not supported, also nudges the element back inside
the window when a flip alone still leaves it hanging over an edge. So the fallback is slightly
more forgiving at an extreme edge, and the modern path is the one that stays correct without
script. The shared behavior is what these docs promise, and neither path ever leaves a floating
element unusable.
The button below asks for placement: 'top'. Click it or Tab to it, since focus
holds a tooltip open where hover would let go. Then scroll the page: as the button nears the top of
your window and the room above runs out, the tooltip flips below, and flips back the moment there
is room above again.
<Button {@attach tooltip({ text: 'Above until there's no room', placement: 'top' })}>
Focus me, then scroll
</Button>Draw your own caret
Nothing here ships a caret. It is a visual decision every design system draws differently, so it
is yours to add. Key it off the resolved [data-side], and push it past the edge
with a negative inset. The floating element is position: fixed in the top layer, so
a protruding caret can never grow the document's own scrollbar. On Popover, draw the caret on .hz-popover-panel itself. That is where data-side lives, and the panel
is kept overflow: visible so a caret protruding past its edge is never clipped. .hz-popover-content, the part that actually scrolls, is the one place a caret would
get cut off.
<Button {@attach tooltip({ text: 'Drawn by this page, not the library', class: 'demo-caret' })}>
Hover for a caret
</Button>
<style>
/* Both elements use the Popover API for the top layer, and the
browser's default [popover] style makes them scroll containers:
without this, a protruding caret is clipped inside instead of
drawn past the edge. (The reference theme already sets it; keep it
if you style from scratch.) */
.hz-tooltip,
.hz-popover-panel {
overflow: visible;
}
.hz-tooltip::after,
.hz-popover-panel::after {
content: '';
position: absolute;
width: 0.5rem;
height: 0.5rem;
background: inherit;
rotate: 45deg;
}
/* data-side is the resolved, PHYSICAL side (it already accounts for
flips and reading direction), so the insets and borders below are
physical too. The negative inset protrudes the caret past the edge;
it can never grow a scrollbar: the floating element is
position: fixed in the top layer. Border only the two edges that
face the trigger. */
[data-side='top']::after {
bottom: -0.25rem;
left: 50%;
translate: -50% 0;
border-right: inherit;
border-bottom: inherit;
}
[data-side='bottom']::after {
top: -0.25rem;
left: 50%;
translate: -50% 0;
border-top: inherit;
border-left: inherit;
}
</style>What placement means for accessibility
Placement never changes DOM order, and DOM order is what decides reading order, focus order, and a menu or listbox's roving-tabindex sequence. A flipped tooltip or an end-aligned menu reads and tabs through in exactly the same order it would unflipped. Flipping is visual only, decided after layout, and it never reorders anything a screen reader or keyboard user would notice.
The top layer is what makes that true. Because a floating element is promoted out of the normal flow rather than moved in the DOM, it can escape a clipping ancestor without its content moving anywhere a keyboard user would have to hunt for it. It is also what keeps a tooltip or menu from being covered by whatever it opened over, which is the substance of WCAG 2.4.11. Two criteria bear on this directly: a tooltip must stay hoverable and dismissible while it is open (1.4.13), and a flipped element must not become the only way to tell two states apart (1.4.1).
References: WCAG 1.4.13 Content on Hover or Focus · WCAG 2.4.11 Focus Not Obscured · MDN: Top layer · MDN: Popover API · MDN: CSS anchor positioning · Can I Use: CSS anchor positioning · Can I Use: popover