Skip to content

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