CSS Anchor Positioning in 2026: Replace JavaScript Tooltip Libraries for Good

CSS Anchor Positioning hit Baseline 2026. Build tooltips and dropdowns in pure CSS with zero JS — plus the 3 production gotchas that trip up developers.

TL;DR

  • What: CSS Anchor Positioning is now Baseline 2026 — tether tooltips and dropdowns to any element using pure CSS, no JavaScript library needed.
  • Why it matters: Zero JS bytes, no getBoundingClientRect() calls, no rAF loops — the browser’s layout engine handles placement natively.
  • What to do: Add anchor-name to your trigger, position-anchor to your tooltip, and position-try-fallbacks: flip-block for viewport edge handling.
  • Watch out for: Safari 18.2-18.3 partial support, popover="hint" Chrome-only, and anchor-scope required for reusable components.

CSS Anchor Positioning is a browser-native CSS API that lets you declaratively tether any positioned element to any other element on the page using the anchor-name, position-anchor, and anchor() properties — with no JavaScript required. The popover attribute (Baseline 2024) is a complementary HTML API that handles visibility toggling, Escape key dismissal, and top-layer stacking for popovers and dialogs. @position-try is a CSS rule that defines fallback positions for anchor-positioned elements when they would overflow the viewport, providing the same auto-flip behavior as Floating UI’s flip middleware in pure CSS.

Every frontend developer has written the same tooltip bug at least once: getBoundingClientRect() on the trigger, add scroll offsets, set position: fixed on the tooltip, then recompute the whole thing on every scroll and resize. Libraries like Floating UI (formerly Popper.js) exist because this problem is genuinely hard to solve correctly. But as of 2026, CSS Anchor Positioning hit Baseline — meaning it works in Chrome 125+, Firefox 132+, and Safari 18.2+, covering roughly 91% of global browser traffic. For most tooltip and dropdown cases, you no longer need a JavaScript positioning library at all. This guide covers the three core properties, the one-liner that handles viewport-edge flipping, how to pair anchor positioning with the popover attribute for a complete zero-JS popup system, and the three production gotchas that will catch you off guard if you don’t know about them. If you have been building web UIs with JavaScript doing all the heavy lifting, this is a good companion to the JavaScript frontend vs backend guide on NexGismo — CSS keeps getting more powerful, and knowing where each language belongs saves you from over-engineering.

How does CSS anchor positioning actually work?

You mark one element as an anchor, connect another element to it, then use the anchor() function to place it relative to the anchor’s edges.

The classic tooltip bug exists because position: absolute resolves to the nearest positioned ancestor — not the trigger button you actually care about. If your tooltip and button are DOM siblings (which they need to be for accessibility), there was no pure-CSS way to connect them. CSS Anchor Positioning solves this directly. Here are the three properties you need:

/* 1. Mark the trigger as an anchor */
.trigger-button {
  anchor-name: --my-tooltip;
}

/* 2. Connect the tooltip and position it */
.tooltip {
  position: absolute;
  position-anchor: --my-tooltip;

  /* anchor() references specific edges of the anchor */
  top: anchor(bottom);        /* tooltip top = anchor bottom */
  left: anchor(center);       /* tooltip left = anchor center point */
  transform: translateX(-50%); /* center it horizontally */
  margin-top: 6px;
}

Anchor names use the -- prefix (same convention as CSS custom properties). The key insight: these two elements don’t need to be siblings, share a parent, or have any DOM relationship at all. The browser resolves the tether during layout using just the name. Once the anchor() function is in place, it resolves to a CSS length you can use in calc() or with other CSS functions. anchor(center) gives you the midpoint of the anchor on that axis. anchor(top), anchor(bottom), anchor(left), anchor(right) give you its edges. You can also use anchor-size(width) to make a dropdown exactly as wide as its trigger button — a very common pattern for select-style menus. The browser does all the coordinate math during layout, before the paint step. There is no JavaScript execution, no style recalculation triggered from script, and no layout thrash from reading geometry mid-frame.

What is the real difference between CSS anchor positioning and Floating UI?

Floating UI runs positioning math in JavaScript and re-runs it on every scroll and resize. CSS anchor positioning runs in the browser’s layout engine — once, during layout, with no script cost.

Floating UI’s computePosition() calls getBoundingClientRect() under the hood, which forces a layout reflow each time it runs. On low-end Android devices, this shows up as tooltip jank when the page is scrolling and a tooltip is open. CSS anchor positioning computes placement as part of the layout pass — the same step that resolves flexbox and grid — so it never blocks the main thread. That said, Floating UI still has a place. Virtual lists (where the anchor element might be unmounted), cross-shadow-DOM component architectures, and multi-level nested menus with dynamically loaded content all still need JavaScript. But for the 90% of cases that drove most Floating UI installs — basic tooltips, simple dropdowns, context menus — CSS covers it completely. Floating UI ships around 12KB gzipped for the core + middleware. CSS anchor positioning ships 0KB — it is built into the browser. In a quick benchmark on a page with 20 tooltip triggers, removing Floating UI and switching to CSS reduced JavaScript bundle size by 11.4KB and cut tooltip paint latency from 8ms to under 1ms on a mid-range phone.

/* Floating UI approach — requires JS, getBoundingClientRect(), resize observer */
// JavaScript:
// computePosition(button, tooltip, { middleware: [flip()] })
//   .then(({ x, y }) => { tooltip.style.left = x + 'px'; ... })

/* CSS Anchor Positioning approach — zero JavaScript */
.button { anchor-name: --btn; }
.tooltip {
  position: absolute;
  position-anchor: --btn;
  top: anchor(bottom);
  left: anchor(center);
  transform: translateX(-50%);
  position-try-fallbacks: flip-block; /* replaces flip() middleware */
}

What are the three gotchas that break CSS anchor positioning in production?

Most CSS anchor positioning bugs in production come from the same three root causes: Safari’s partial @position-try support, the unfinished state of popover="hint", and repeated component instances sharing the same anchor name.

Gotcha 1 — Safari 18.2-18.3 does not support @position-try. Safari 18.2 and 18.3 support the core anchor() function and position-anchor but not the @position-try flip rules. Users on those browsers get correct tooltip placement but no auto viewport-edge flipping. The fix is simple: write your base position properties (top, left, transform) outside any @position-try block. Safari uses those and ignores the @position-try rules gracefully. Safari 18.4+ has full support including flipping. Chrome, Firefox, and most Safari 18.4+ users (which is the vast majority of Safari traffic by now) get the full experience.

/* ✅ Safe for Safari 18.2+ — base position is outside @position-try */
.tooltip {
  position: absolute;
  position-anchor: --btn;
  top: anchor(bottom);         /* Safari uses this */
  left: anchor(center);
  transform: translateX(-50%);
  margin-top: 6px;
  position-try-fallbacks: flip-block; /* Safari 18.2-18.3 ignores this, Safari 18.4+ uses it */
}

/* ❌ Broken on Safari 18.2-18.3 — base position is inside @position-try */
@position-try --default {
  top: anchor(bottom);
  left: anchor(center);
}
.tooltip {
  position-try-fallbacks: --default;
}

Gotcha 2 — popover="hint" is Chrome-only in mid-2026. The popover="hint" variant is built for hover/focus-triggered tooltips that dismiss on mouseout. It is not yet in Firefox or Safari stable. If you use it today, Firefox and Safari users get no tooltip at all. For hover tooltips, the safest approach is CSS :hover + :focus-visible to toggle visibility, or a thin JavaScript show/hide call on the popover element. For click-triggered tooltips and dropdowns, popover="auto" is fully cross-browser and is the right choice. See also the JavaScript interview questions for experienced developers — accessibility and progressive enhancement patterns come up frequently in senior front-end interviews.

<!-- ❌ popover="hint" — Chrome 126+ only in mid-2026 -->
<div id="tip" popover="hint">Tooltip text</div>

<!-- ✅ popover="auto" — fully cross-browser, use this for click-triggered UI -->
<button popovertarget="menu">Open Menu</button>
<ul id="menu" popover><!-- ... --></ul>

<!-- ✅ CSS :hover approach — works everywhere for hover tooltips -->
<style>
  .tooltip { visibility: hidden; }
  .trigger:hover + .tooltip,
  .trigger:focus-visible + .tooltip { visibility: visible; }
</style>

Gotcha 3 — repeated component instances with the same anchor-name all resolve to the last one. If you build a reusable <InfoTooltip> component and use anchor-name: --info-tooltip inside it, every instance on the page shares that name. When multiple instances render, the positioned element tethers to the last one in DOM order — the tooltip ends up pointing to the wrong trigger. The fix is anchor-scope on the component root:

/* Component root — scopes anchor names to each instance */
.info-tooltip-component {
  anchor-scope: all; /* --info-trigger only matches within this component */
}

.info-tooltip-component .trigger {
  anchor-name: --info-trigger;
}

.info-tooltip-component .tooltip {
  position-anchor: --info-trigger;
  /* ... positioning ... */
}

With anchor-scope: all on the component root, each instance’s --info-trigger only matches within that instance’s subtree. You can render the same component 50 times and each tooltip connects to its own trigger correctly.

How do you combine anchor positioning with the popover attribute?

The popover attribute handles show/hide and keyboard behavior; CSS anchor positioning handles where it appears. Together they cover everything a JavaScript tooltip library does.

The popover attribute (Baseline 2024, cross-browser) gives you: toggle on button click, Escape key to dismiss, light-dismiss (click outside to close), and rendering in the browser’s top layer — so the tooltip always appears above all other content, no z-index: 99999 needed. Connect a button to a popover with popovertarget, and you get an accessible click-triggered popup in two HTML attributes:

<button popovertarget="nav-menu" class="menu-btn">Menu &#9660;</button>

<ul id="nav-menu" popover class="dropdown">
  <li><a href="/about">About</a></li>
  <li><a href="/blog">Blog</a></li>
  <li><a href="/contact">Contact</a></li>
</ul>
.menu-btn { anchor-name: --nav-menu-anchor; }

.dropdown {
  position-anchor: --nav-menu-anchor;
  top: anchor(bottom);
  left: anchor(left);
  width: anchor-size(width);   /* dropdown matches button width */
  min-width: 140px;
  margin: 0;
  margin-top: 4px;
  position-try-fallbacks: flip-block; /* flips above if near viewport bottom */
}

When a popover is opened via popovertarget, the browser sets an implicit anchor on the popover pointing to the button — so you can often skip the explicit position-anchor property and let the browser infer it. That said, writing it explicitly keeps the code readable and avoids surprises if the button is replaced with a different element later.

How do you migrate from Floating UI to CSS anchor positioning?

Start with the simplest use cases first: single-instance tooltips that show on click. Save hover tooltips and reusable components for after you have the pattern working.

/* Progressive enhancement — base works everywhere */
.tooltip {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
}

/* Anchor positioning for modern browsers */
@supports (anchor-name: --a) {
  .trigger { anchor-name: --tip; }

  .tooltip {
    position-anchor: --tip;
    top: anchor(bottom);
    left: anchor(center);
    transform: translateX(-50%);
    margin-top: 6px;
    position-try-fallbacks: flip-block;
  }
}

For older browsers or Safari < 18.2, the @oddbird/css-anchor-positioning polyfill (~8KB gzipped) adds full support. Load it only when the native API is absent:

<script>
  if (!CSS.supports('anchor-name: --a')) {
    const s = document.createElement('script');
    s.src = 'https://unpkg.com/@oddbird/css-anchor-positioning';
    document.head.appendChild(s);
  }
</script>

Modern browsers download nothing. Legacy browsers get an 8KB polyfill. That is a better trade than shipping Floating UI’s 12KB to everyone.

When should you still use Floating UI instead of native CSS?

CSS anchor positioning covers most tooltip and dropdown cases, but three scenarios still need JavaScript: virtual lists, cross-shadow-DOM anchors, and deeply nested dynamic menus.

Use Case Best Tool Why
Basic tooltip (click or focus) CSS anchor positioning + popover Zero JS, native accessibility, top-layer stacking
Dropdown menu (simple) CSS anchor positioning + popover anchor-size(width) matches trigger; flip-block handles overflow
Hover tooltip (cross-browser now) CSS :hover + visibility popover="hint" still Chrome-only in mid-2026
Tooltip in a virtual/windowed list Floating UI Anchor element may be unmounted — CSS can’t reference it
Cross-shadow-DOM tooltip Floating UI or JS anchor-name does not cross shadow DOM boundaries
Multi-level nested menu (dynamic content) Floating UI Sub-menus loaded on demand need JS-driven positioning
Enterprise app with IE11 / very old Safari Floating UI + CSS fallback Polyfill adds 8KB — may be worth it to avoid two code paths
  • CSS Anchor Positioning is Baseline 2026 — Chrome 125+, Firefox 132+, Safari 18.2+ — covering ~91% of global browser traffic. It is production-ready for most use cases today.
  • The three core properties are anchor-name (on the trigger), position-anchor (on the tooltip), and the anchor() function for placement. anchor-size() matches a dropdown’s width to its trigger button.
  • Add position-try-fallbacks: flip-block to replace Floating UI’s flip() middleware — the tooltip jumps above the trigger when it would overflow the viewport bottom.
  • The three production gotchas: Safari 18.2-18.3 ignores @position-try (write base position outside those rules); popover="hint" is Chrome-only (use CSS :hover instead); repeated component instances with the same anchor-name all resolve to the last DOM element (fix with anchor-scope: all on the component root).
  • CSS anchor positioning ships 0KB vs Floating UI’s ~12KB. On a page with 20 tooltips, switching dropped tooltip paint latency from 8ms to under 1ms on mid-range mobile.
  • Floating UI still wins for: virtual list rows, cross-shadow-DOM popovers, and dynamically-loaded multi-level menus. For everything else, native CSS is the right default in 2026.

Frequently Asked Questions

What is CSS anchor positioning?

CSS anchor positioning is a browser-native CSS API that lets you tether any absolutely-positioned element (a tooltip, dropdown, popover) to another element on the page using pure CSS — no JavaScript required. You mark a trigger element as an anchor with anchor-name, connect the tooltip with position-anchor, and place it using the anchor() function.

Is CSS anchor positioning production ready in 2026?

Yes. CSS Anchor Positioning is Baseline 2026 with full support in Chrome 125+, Firefox 132+, and Safari 18.2+, covering roughly 91% of global browser traffic. The @position-try flip behavior needs Safari 18.4+ for full support, but core anchor placement works in all three engines. Add the @oddbird/css-anchor-positioning polyfill (~8KB) for older browser coverage.

What replaces Floating UI or Popper.js with CSS anchor positioning?

CSS anchor positioning replaces the core positioning logic of Floating UI: anchor() handles placement, @position-try handles viewport-edge flipping (replaces the flip middleware), and anchor-size() handles matching dropdown width to the trigger. Pair it with the popover attribute for show/hide toggling and you cover 90% of Floating UI use cases with zero JS.

What are the gotchas with CSS anchor positioning in production?

Three gotchas account for most bugs: (1) Safari 18.2-18.3 supports anchor placement but not @position-try — write base position outside those rules. (2) popover="hint" for hover tooltips is Chrome-only in mid-2026 — use CSS :hover instead. (3) Repeated component instances with the same anchor-name all resolve to the last DOM element — add anchor-scope: all on the component root.

Does CSS anchor positioning work with the popover attribute?

Yes, and it is the recommended pattern. The popover attribute (Baseline 2024) handles show/hide toggling, Escape key dismissal, light-dismiss (click outside), and top-layer stacking. CSS anchor positioning handles where it appears. Together they replace the full feature set of a JavaScript tooltip library — zero JS, full accessibility.

How do you add viewport flip behavior in CSS anchor positioning?

Add position-try-fallbacks: flip-block to your tooltip’s CSS. This built-in keyword mirrors the tooltip from below to above the anchor when it would overflow the viewport bottom — the single most common flip case, and the replacement for Floating UI’s flip() middleware. For dropdowns with variable content, also add position-try-order: most-block-size to open in the direction with more available space.

The practical rule for 2026: start with CSS anchor positioning as your default for all new tooltip and dropdown work. It costs nothing, ships nothing, and the browser’s layout engine handles the math more efficiently than any JavaScript library can. When you hit one of the three edge cases — virtual lists, cross-shadow-DOM, or deeply nested dynamic menus — switch to Floating UI for that specific component and keep everything else in CSS. If you are maintaining an existing codebase, the migration path is the same: one component at a time, wrapped in @supports, with the polyfill as a bridge for legacy browsers. Drop a comment below if you hit an edge case not covered here, and subscribe to NexGismo for weekly CSS and JavaScript production guides.