Home / Snippets / UI Components /

Anchor-positioned tooltip

Tooltip tethered via anchor-name and anchor() — replaces Popper.js.

Hover me (above) Positioned above the trigger Hover me (above) Another anchor, same CSS pattern Hover me (below) Positioned below the trigger
Experimental
uiexperimental

Quick implementation

/* 1. Name the anchor element */
.trigger { anchor-name: --my-tooltip; }

/* 2. Position the tooltip relative to that anchor */
.tooltip {
  position: fixed;
  /* Place bottom of tooltip at top of anchor */
  inset-block-end: anchor(--my-tooltip top);
  /* Center horizontally on anchor */
  inset-inline-start: anchor(--my-tooltip center);
  translate: -50% -100%;
  margin-block-end: 0.4rem; /* gap above anchor */

  /* Style */
  padding: 0.35rem 0.65rem;
  background: oklch(0.18 0.025 260);
  color: white; font-size: 0.78rem;
  border-radius: 0.375rem; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity 0.15s;
}
.trigger:hover + .tooltip { opacity: 1; }

/* 3. Fallback for unsupported browsers */
@supports not (anchor-name: --x) {
  .trigger { position: relative; }
  .tooltip {
    position: absolute;
    bottom: calc(100% + 0.4rem); left: 50%;
    translate: -50% 0;
    inset-block-end: auto; inset-inline-start: auto;
  }
}

Prompt this to your LLM

Includes above/below placement, arrow, and a React variant with Popover API.

You are a senior frontend engineer experimenting with cutting-edge CSS.

Goal: Build a tooltip that uses CSS Anchor Positioning to stay tethered to its trigger — no JavaScript for placement.

Technical constraints:
- Assign anchor-name: --tooltip-trigger on the trigger element.
- On the tooltip: position: fixed; inset-block-end: anchor(--tooltip-trigger top); inset-inline-start: anchor(--tooltip-trigger center); translate: -50% -100%.
- Add a small gap (margin-block-end) between tooltip and trigger.
- Include a CSS triangle arrow using ::after (border trick or clip-path).
- Show/hide with opacity 0 → 1 on :hover (and :focus-visible for keyboard users).

Placement variants:
A) Above (default): inset-block-end at anchor top.
B) Below: inset-block-start at anchor bottom.

Framework / Popover API variant (bonus):
- Pair with the HTML Popover API (<button popovertarget> + <div popover>) so the tooltip has proper open/close state and is accessible by default.

Edge cases to handle:
- Fallback for browsers that don't support anchor(): @supports not (anchor-name: --x) { use position: absolute on a positioned parent instead }.
- Note that anchor-name takes a dashed-ident (e.g. --my-anchor), not a plain name.
- Mention that overflow: hidden on any ancestor will clip the tooltip — use position: fixed to escape stacking contexts.

Return full HTML + CSS with both placement variants and the @supports fallback.

Why this matters in 2026

Floating UI (tooltips, popovers, select dropdowns) has always needed JavaScript for placement — libraries like Popper.js exist entirely for this problem. CSS Anchor Positioning is the native solution: you name an element, and another element positions itself relative to that name. The browser handles scroll, resize, and overflow avoidance.

Browser support is still rolling out (Chrome/Edge, Safari coming), but with a clean @supports fallback it's safe to ship today as a progressive enhancement.

The logic

The anchor element gets anchor-name: --tooltip-trigger (a dashed-ident). The tooltip is position: fixed with logical-property variants: inset-block-end: anchor(--tooltip-trigger top) aligns the tooltip's bottom edge to the trigger's top edge. inset-inline-start: anchor(--tooltip-trigger center) sets its left edge to the trigger's center. Then translate: -50% -100% centers it horizontally and lifts it above.

Using position: fixed means the tooltip escapes any overflow or stacking-context clips on ancestor elements.

Accessibility & performance

Show tooltips on focus-visible too — keyboard users need them. For critical content, don't hide it in a tooltip; tooltips are supplemental. Pair with the Popover API for proper ARIA (popover attribute gives you role and aria-expanded for free). The CSS-only show/hide is GPU-composited — no layout recalc, no JS on the main thread.

Browser support note: anchor-name is in Chrome 125+, Edge 125+. Safari support is landing. Always include @supports not (anchor-name: --x) fallback.