Contents

CSS Anchor Positioning: Replace Floating UI With CSS

CSS Anchor Positioning is a browser-native feature that pins any absolutely-placed element to another element in the document. No JavaScript required. With the anchor() function, position-anchor property, and @position-try rules, you can build tooltips, dropdown menus, and context menus in pure CSS and HTML. It now works in Chrome 125+, Firefox 132+, and Safari 18.2+, which covers about 91% of browser traffic. Pair it with the HTML popover attribute (Baseline 2024) and you get show/hide toggling, keyboard dismissal, and top-layer stacking for free. The JavaScript tooltip library is dead for most use cases.

The Problem CSS Anchor Positioning Solves

Before CSS Anchor Positioning, placing a tooltip next to a button needed JavaScript. The recipe looked like this: call getBoundingClientRect() on the trigger, add scroll offsets, then set position: fixed with computed top and left values on the tooltip. You then had to redo the math on every scroll, every resize, and every orientation change on mobile. Libraries like Floating UI (formerly Popper.js) exist because this problem is hard to get right.

The core snag is that position: absolute resolves to the nearest positioned ancestor, not the trigger element. If the tooltip and its trigger button are siblings in the DOM (which they usually need to be for accessibility), the tooltip has no CSS-native way to find the button. Add a wrapper div somewhere up the tree, or change an ancestor’s position, and the tooltip drifts to the wrong spot. Every frontend dev has debugged this at least once.

CSS Anchor Positioning fixes this by tying two elements together. You mark one element as an “anchor” with anchor-name: --my-anchor, and another element refers to it with position-anchor: --my-anchor. The positioned element can then use the anchor() function in its position values. Writing top: anchor(bottom) means “my top edge aligns with my anchor’s bottom edge.” The browser does all the coordinate math during layout, with zero JavaScript overhead.

Browser support in 2026 is solid. Chrome and Edge have had full support since version 125. Firefox shipped it without flags in version 132. Safari 18.2+ supports the core anchor() function and position-anchor, though @position-try (the flip behavior) needs Safari 18.4+. For Safari 18.2-18.3 users, you get correct placement but no auto viewport flipping. That is a fair trade.

JavaScript positioning still has a place. Deep nested menus with loaded-on-demand content, tooltips inside shadow DOM with cross-origin limits, and virtualized lists where the anchor might be unmounted from the DOM all push you back to a JS solution. But for most tooltip and dropdown cases (the ones that drive 90% of Floating UI installs) CSS Anchor Positioning handles it all.

Anchor positioning and the HTML popover attribute solve two halves of the same problem. The popover attribute handles visibility toggling, Escape key dismissal, light-dismiss (clicking outside), and rendering in the browser’s top layer. CSS Anchor Positioning handles where the popup shows up on screen. Together, they cover every job a JavaScript tooltip library used to do.

Core Syntax: anchor-name, position-anchor, and anchor()

The CSS Anchor Positioning API has three core building blocks. They cover the full range of tooltip and dropdown placement needs.

Defining an Anchor

To mark an element as an anchor, add the anchor-name property with a dashed-ident value:

.trigger-button {
  anchor-name: --button-anchor;
}

Anchor names use the -- prefix, just like CSS custom properties. You can set many anchors on different elements across the page, each with a unique name. Any element can be an anchor: buttons, links, table cells, icons, whatever you need.

Referencing an Anchor

On the element you want to position (the tooltip, dropdown, or popover), set position-anchor to the anchor name. Make sure the element has position: absolute or position: fixed:

.tooltip {
  position: absolute;
  position-anchor: --button-anchor;
}

The positioned element is now tied to the anchor no matter where they sit in the DOM tree. They don’t need to be siblings or share any parent-child link.

The anchor() Function

The anchor() function goes inside position properties (top, right, bottom, left) to point to specific edges or the center of the anchor:

.tooltip {
  position: absolute;
  position-anchor: --button-anchor;
  top: anchor(bottom);       /* tooltip's top = anchor's bottom */
  left: anchor(center);      /* tooltip's left = anchor's center */
  transform: translateX(-50%);
  margin-top: 4px;
}

This puts the tooltip right below the button, centered side to side. The anchor(center) value resolves to the midpoint of the anchor on that axis. You can mix anchor() with calc() for tighter control. left: calc(anchor(center) - 50%) is another way to center the tooltip.

Sizing with anchor-size()

The anchor-size() function sizes a positioned element from its anchor’s box:

.dropdown-menu {
  width: anchor-size(width);
}

This makes the dropdown exactly as wide as its trigger button. It is a common pattern for select-style dropdowns. The function takes width, height, inline, block, self-inline, and self-block as arguments.

Logical Properties

For UIs that handle right-to-left text or vertical writing modes, use logical property names instead of physical ones:

.tooltip {
  inset-block-start: anchor(block-end);   /* equivalent to top: anchor(bottom) */
  inset-inline-start: anchor(inline-start); /* equivalent to left: anchor(left) */
}

Complete Tooltip Example

Here is a working tooltip in pure CSS:

.trigger {
  anchor-name: --tip-anchor;
}

.tooltip {
  position: absolute;
  position-anchor: --tip-anchor;
  top: anchor(bottom);
  left: anchor(center);
  transform: translateX(-50%);
  margin-top: 4px;
  background: #1a1a2e;
  color: #fff;
  padding: 6px 10px;
  border-radius: 4px;
  font-size: 0.85rem;
  white-space: nowrap;
}

That gets you a working tooltip with no event listeners, no getBoundingClientRect() calls, and no resize observers. The browser handles all of it during layout.

Flip Behavior with @position-try and position-try-fallbacks

A tooltip pinned below a button works fine until the button sits near the bottom of the viewport. At that point, the tooltip gets clipped or overflows. The old fix needs a JavaScript resize observer that finds free space and flips the tooltip above the trigger. CSS Anchor Positioning handles this natively with @position-try rules.

Defining Fallback Positions

A @position-try block defines an alternate positioning strategy:

@position-try --flip-above {
  top: auto;
  bottom: anchor(top);
  margin-top: 0;
  margin-bottom: 4px;
}

This fallback says: instead of placing the tooltip below the anchor, place it above. The position-try-fallbacks property on the positioned element lists fallbacks in the order you prefer:

.tooltip {
  position: absolute;
  position-anchor: --tip-anchor;
  top: anchor(bottom);
  left: anchor(center);
  transform: translateX(-50%);
  margin-top: 4px;

  position-try-fallbacks: --flip-above;
}

The browser draws the tooltip in the base position first. If it would overflow the viewport, the browser tries --flip-above. If that also overflows, it falls back to the base spot (clipped). You can chain many fallbacks:

position-try-fallbacks: --flip-above, --shift-right, --shift-left;

Built-in Flip Keywords

For the most common case (mirroring the position on one axis) there are shorthand keywords that save you from writing custom @position-try blocks:

.tooltip {
  position-try-fallbacks: flip-block;
}

flip-block builds the mirrored version of the base position on the block axis (top becomes bottom, bottom becomes top). flip-inline does the same on the inline axis. For most tooltips, flip-block is all you need.

Controlling Fallback Selection with position-try-order

The position-try-order property changes how the browser picks between fallbacks:

.dropdown-menu {
  position-try-order: most-block-size;
  position-try-fallbacks: flip-block;
}

most-block-size tells the browser to pick the position with the most free vertical space. This helps dropdown menus that hold a variable number of items. The menu opens in the direction with more room.

Production-Ready Flip Configuration

A solid tooltip setup in production usually needs three or four fallbacks. The base position (below-center), a block flip (above-center), and shift fallbacks for buttons near the left or right viewport edges:

@position-try --flip-above {
  top: auto;
  bottom: anchor(top);
  margin-bottom: 4px;
}

@position-try --shift-right {
  left: anchor(left);
  transform: none;
}

@position-try --shift-left {
  left: auto;
  right: anchor(right);
  transform: none;
}

.tooltip {
  position: absolute;
  position-anchor: --tip-anchor;
  top: anchor(bottom);
  left: anchor(center);
  transform: translateX(-50%);
  margin-top: 4px;

  position-try-fallbacks: --flip-above, --shift-right, --shift-left;
}

This covers nearly every viewport case without a single line of JavaScript.

Combining Anchor Positioning with the HTML popover Attribute

The HTML popover attribute (Baseline 2024, in all major browsers) handles the other half of the popup problem. It owns visibility toggling, keyboard input, and stacking context. Pair it with CSS Anchor Positioning and you get a fully declarative, accessible popup system.

Popover Basics

Add the popover attribute to any element to make it a popover. Add popovertarget to a button to build the toggle control:

<button popovertarget="my-tooltip">Hover me</button>
<div id="my-tooltip" popover>This is a tooltip</div>

The browser handles Escape key dismissal, light dismiss (clicking outside the popover), and renders the popover in the top layer . The popover sits above all other content, no matter what z-index values appear elsewhere in the document. No more z-index: 99999 hacks.

Connecting Popover to Anchor

Combine the popover HTML with anchor positioning CSS:

<button popovertarget="tip" class="trigger">Info</button>
<div id="tip" popover class="tooltip">Helpful information here</div>
.trigger {
  anchor-name: --trigger;
}

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

Clicking the button toggles the tooltip. Pressing Escape closes it. Clicking outside closes it. The tooltip shows up right below the button and flips above when there is not enough room. All without a single line of JavaScript.

Backdrop Customization

Popovers in the top layer can show a backdrop overlay:

[popover]::backdrop {
  background: rgba(0, 0, 0, 0.3);
}

For a frosted glass effect, add backdrop-filter: blur(4px). This works well for modal-style dialogs that use anchor positioning to stay pinned to their trigger.

Accessibility

The popover attribute with popover=auto gives built-in dialog semantics. Pair it with aria-labelledby to give screen readers a proper title:

<button popovertarget="info-panel" aria-expanded="false">Details</button>
<div id="info-panel" popover aria-labelledby="panel-title">
  <h3 id="panel-title">Additional Details</h3>
  <p>Content here...</p>
</div>

Keyboard users open the popover with Enter or Space on the trigger button and close it with Escape. The browser handles focus management.

The popover=hint Variant

Chrome 126+ added popover=hint, a variant built for tooltips that show on hover or focus and dismiss on mouseout. It is still behind a flag in some browsers, but the spec is firming up fast:

<button popovertarget="hover-tip">?</button>
<div id="hover-tip" popover="hint">Explanation text</div>

Once browser support catches up, popover=hint will cover the hover-triggered tooltip case that still needs a bit of JavaScript today.

Full Dropdown Menu Example

Here is a full dropdown menu in about 10 lines of HTML with no JavaScript:

<button popovertarget="nav-menu" class="menu-trigger">Menu</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-trigger {
  anchor-name: --nav-trigger;
}

.dropdown {
  position-anchor: --nav-trigger;
  top: anchor(bottom);
  left: anchor(left);
  margin: 0;
  margin-top: 2px;
  width: anchor-size(width);
  min-width: 150px;
  position-try-fallbacks: flip-block;
}

The dropdown matches the trigger button’s width (with a 150px floor). It sits right below the button and flips above when near the viewport bottom. The browser handles open/close toggling, keyboard moves, and stacking.

Progressive Enhancement and Fallback Strategies

Support is broad in 2026, but Safari 18.2-18.3 ships only part of @position-try, and older browsers still linger on enterprise machines. Fallbacks are still a must for production sites.

Feature Detection with @supports

Wrap anchor positioning CSS in a @supports block so older browsers fall back cleanly:

/* Base fallback: simple static positioning */
.tooltip {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
}

/* Enhanced: use anchor positioning when supported */
@supports (anchor-name: --a) {
  .tooltip {
    position-anchor: --tip-anchor;
    top: anchor(bottom);
    left: anchor(center);
    transform: translateX(-50%);
    position-try-fallbacks: flip-block;
  }
}

Browsers without anchor positioning get a tooltip that drops below its parent element using plain CSS. Not perfect, but it works. The same @supports trick fits other modern layout features like CSS container queries .

Safari 18.2-18.3 Handling

Safari 18.2 and 18.3 support anchor() and position-anchor but not @position-try. The practical trick is to write your base position outside any @position-try blocks. Safari then draws the tooltip in the right spot, just without auto viewport flipping. Chrome, Firefox, and Safari 18.4+ get the full flip behavior. Most users will never notice the gap.

JavaScript Polyfill Option

The community-maintained @oddbird/css-anchor-positioning polyfill adds anchor() and @position-try support for browsers without native code. It is about 8KB gzipped and can load on demand:

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

This way, modern browsers pay zero JavaScript cost. Only legacy browsers download the polyfill.

Floating UI as a Fallback

For projects that must support Safari older than 18.2 with precise viewport-edge logic, Floating UI 3.0 (the heir to Popper.js) works as a targeted polyfill. Use CSS Anchor Positioning as your main path and load Floating UI only for legacy browsers. This keeps the modern code path clean and avoids shipping extra JavaScript to 91% of your users.

Performance Comparison

CSS Anchor Positioning runs in the browser’s layout engine. There is no JavaScript work, no requestAnimationFrame loops, and no layout thrash from getBoundingClientRect() calls. Floating UI’s computePosition() runs in the microtask queue and triggers a layout reflow each time it runs the math. On low-end mobile devices, the gap shows up. CSS-based placement gives smoother scroll performance and uses less battery. On desktop, both feel instant, but the CSS path ships zero extra bytes.

Testing Your Implementation

Test anchor positioning across browsers with this checklist:

  • In Chrome DevTools, resize the viewport to trigger flip behavior and check that @position-try fallbacks kick in
  • Use Safari Technology Preview to test Safari 18.4+ @position-try support
  • Check Firefox Developer Edition for full feature support without flags
  • On mobile, use BrowserStack or real devices to test touch input and viewport flipping on scroll

The CSS Anchor Positioning spec is stable and well-supported for production use in 2026. For new projects, there is no reason to reach for a JavaScript positioning library first. Write your tooltips and dropdowns in CSS, add @position-try for viewport flipping, pair with the popover attribute for input handling, and ship. The browser does the rest. If you build the same way across your UI, dark mode in vanilla CSS is another native feature worth adding with no framework.