NP
← Back to Blog
CSSWeb DevelopmentFrontendInteractive

I Told You
to Wait. CSS Anchor Positioning Is Here.

NP

Nick Paolini

April 29, 2026

7 min read read

In my Modern CSS Toolkit for 2026 post in January, I had a "Not Yet" section. Anchor positioning was on it. My exact words: "Chrome only, wait for broader support."

It's April. Stop waiting.

As of March 2026, CSS Anchor Positioning is Baseline. Chrome 125 or later, Firefox 147 or later, Safari 26 or later — full support, no flags. The only catch is Safari 18.2–18.3 ships the core anchor() function but not @position-try fallbacks, which means you get correct positioning but not automatic viewport-edge flipping. That's a perfectly graceful degradation for the few users who haven't updated.

Translation: you can delete Floating UI, Popper.js, and Tippy.js from your bundle this week. Let me show you what replaces them.

What Anchor Positioning Actually Does

Anchor positioning lets you tether one element to another using pure CSS. You designate an element as an anchor with anchor-name, then any positioned element can attach to it with position-anchor and the anchor() function.

That's the whole concept. The browser handles the layout math — including scroll, resize, and overflow — natively, on the compositor thread, without a single JavaScript listener.

.button {
  anchor-name: --my-button;
}
 
.tooltip {
  position: absolute;
  position-anchor: --my-button;
  top: anchor(bottom);
  left: anchor(center);
}

That's a tooltip. It will follow .button around forever. No getBoundingClientRect(). No scroll handlers. No ResizeObserver. No 13KB library.

Demo 1: The Hello World

Drag me
I follow

Try dragging the anchor. The tooltip follows automatically — no scroll listeners, no JavaScript layout math. The browser handles it.

The Shorthand That Changes Everything: position-area

You'll rarely write top: anchor(bottom); left: anchor(center) in real code. The position-area property gives you a 3×3 grid of positioning regions relative to the anchor:

.tooltip {
  position: fixed;
  position-anchor: --my-button;
  position-area: top center;  /* directly above */
}

Valid values: top, bottom, left, right, center, plus combinations like top left, bottom right, top span-all, start end (logical), and so on. There are 27 distinct positioning regions. You almost never need raw anchor() calls.

Demo 2: Position Playground

Anchor
Tooltip
position-area: top center

27 distinct positioning regions, one property. The start/end values respect writing direction automatically.

The Killer Feature: position-try-fallbacks

Here's where this stops being "neat" and starts being "actually replaces JavaScript."

When a tooltip is anchored to a button near the bottom of the viewport, it should flip up to stay visible. This is what Floating UI's entire 47KB bundle exists to do. In modern CSS, it's one line:

.tooltip {
  position: fixed;
  position-anchor: --my-button;
  position-area: top center;
  position-try-fallbacks: flip-block;  /* flip vertically if it overflows */
}

Want more sophisticated fallback chains? Define them with @position-try:

@position-try --bottom-fallback {
  position-area: bottom center;
}
 
@position-try --side-fallback {
  position-area: right center;
  margin-left: 8px;
}
 
.tooltip {
  position-anchor: --my-button;
  position-area: top center;
  position-try-fallbacks: --bottom-fallback, --side-fallback, flip-inline;
}

The browser tries each fallback in order and uses the first one that fits in the viewport. This is what every popover library you've ever installed was built to do.

Demo 3: The Edge-Aware Tooltip

Fallbacks:
Drag me
Edge-aware tooltip
Drag anchor near edges →

This is the exact behavior every JavaScript tooltip library was built to provide. One CSS property does it now.

The Combo: Anchor Positioning + the popover Attribute

Anchor positioning becomes truly powerful when you pair it with the native HTML popover attribute. You get accessibility, top-layer rendering, light dismiss, and ESC-to-close — all for free, all without JavaScript:

<button popovertarget="user-menu" id="trigger">Account</button>
 
<div popover id="user-menu" anchor="trigger">
  <a href="/profile">Profile</a>
  <a href="/settings">Settings</a>
  <a href="/logout">Log out</a>
</div>
#trigger {
  anchor-name: --trigger;
}
 
#user-menu {
  position-anchor: --trigger;
  position-area: bottom span-right;
  margin-top: 4px;
  position-try-fallbacks: flip-block;
}
 
/* Smooth open/close animation */
#user-menu {
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity 0.2s, transform 0.2s, overlay 0.2s allow-discrete, display 0.2s allow-discrete;
}
 
#user-menu:popover-open {
  opacity: 1;
  transform: translateY(0);
}
 
@starting-style {
  #user-menu:popover-open {
    opacity: 0;
    transform: translateY(-8px);
  }
}

That is a complete production-ready dropdown menu. Click the button, it opens. Click outside, it closes. Press ESC, it closes. Tab navigation works. Screen readers announce it correctly. Zero JavaScript.

Demo 4: The Native Dropdown

Zero JavaScript. Full keyboard navigation. Light dismiss. Top-layer rendering. This is the entire dropdown.

Sizing with anchor-size()

The anchor-size() function lets the positioned element borrow dimensions from its anchor. Useful for things like dropdown menus that should match the width of their trigger:

.search-suggestions {
  position-anchor: --search-input;
  position-area: bottom span-all;
  width: anchor-size(width);  /* match the input's width exactly */
  max-height: calc(anchor-size(height) * 8);  /* 8x the input height */
}

This kills another thing libraries used to do — measuring an element to size a related element.

Demo 5: The Range Slider Value Bubble

50
Drag the slider to see the bubble follow

The thumb is a pseudo-element. You can still anchor to it. anchor-size() lets the bubble borrow the thumb's dimensions for sizing calculations.

Where It Gets Genuinely Cool: Dynamic Anchors

You can change which element a positioned element is anchored to with CSS alone. Combined with :has(), you get effects that previously required full state-management libraries:

.tab-bar {
  position: relative;
}
 
.tab {
  anchor-name: --tab;
}
 
.tab:has(input:checked) {
  anchor-name: --active-tab;
}
 
.tab-indicator {
  position: absolute;
  position-anchor: --active-tab;
  left: anchor(left);
  width: anchor-size(width);
  bottom: 0;
  height: 2px;
  background: var(--primary);
  transition: left 0.3s, width 0.3s;
}

The indicator slides between tabs as the user clicks. No JavaScript. No useState. No useRef. The browser interpolates the anchor change because the resolved values are still numeric.

Demo 6: The Magnetic Tab Indicator

Home Content

The indicator animates smoothly as you switch tabs. No JavaScript animation code—the browser interpolates the anchor position change.

Note that the same --d6-tab name is used by every tab — the active tab gets a more specific name via :has(). The indicator anchors to whichever element currently holds --d6-active-tab. The browser interpolates the change.

Pitfalls I've Already Hit

1. The anchor element must be in scope. Anchor positioning has scoping rules. The positioned element generally needs to be a descendant of, or in the same containing block as, the anchor — unless the positioned element is in the top layer (popover, dialog), in which case scoping is much more permissive. If your tooltip isn't tethering and devtools shows the property as valid but the position is wrong, this is almost always why.

2. position-area requires position: absolute or fixed. It quietly does nothing on statically positioned elements. No error, no warning.

3. Don't combine position-area with top/left/right/bottom. They fight. position-area already implies inset values. Use one approach or the other.

4. Safari 18.2 and 18.3 don't support @position-try. The core anchoring works, but custom fallbacks won't fire. Default to flip-block and flip-inline keywords (which they do support) and only reach for @position-try rules when you genuinely need custom behavior.

5. anchor-name is not inheritable. Each anchor needs its name declared explicitly. This trips people up coming from CSS variable patterns.

The Fallback Story

For the 5–10% of users still on older Safari or enterprise-locked browsers, wrap your anchor positioning in @supports:

/* Baseline: works everywhere */
.tooltip {
  position: absolute;
  bottom: 100%;
  left: 50%;
  transform: translateX(-50%);
}
 
/* Enhanced: anchor positioning when available */
@supports (anchor-name: --x) {
  .tooltip {
    position: fixed;
    position-anchor: --my-button;
    position-area: top center;
    position-try-fallbacks: flip-block;
    inset: auto;       /* reset the absolute positioning */
    transform: none;
  }
}

The unsupported browser gets a tooltip that appears above its parent — not perfectly viewport-aware, but functional. Everyone else gets the real thing.

If you absolutely need full feature parity on older browsers, Oddbird's polyfill is solid. But honestly, for new projects in April 2026, I'd just @supports it and move on.

Decision Framework: Should You Adopt This?

Same three questions from the toolkit post:

Browser support? Baseline 2026 (approximately 92% global). ✅ Use freely.

Fallback cost? Static absolute positioning works fine for the long tail. ✅ Acceptable.

Eliminates complexity elsewhere? Replaces an entire category of JavaScript libraries (Floating UI, Popper.js, Tippy.js, Headless UI's positioning logic, Radix's positioning logic). ✅ Massively.

This is a yes, today feature.

What I'm Deleting from My Projects This Week

  • @floating-ui/react (47KB → 0)
  • All scroll listeners that update tooltip positions
  • All ResizeObserver instances that re-measure for popovers
  • The 200-line usePosition hook from a project I started in 2024
  • An entire Tooltip.tsx file replaced by 12 lines of CSS

The toolkit post made the case that modern CSS lets you ship less code. Anchor positioning is the most extreme example of that thesis I've seen. It deletes whole categories of dependencies, not just individual utilities.

Bottom Line

The tooltip positioning war is over. CSS won.

If you're starting a new project: use anchor positioning, no library.

If you're maintaining an existing project: migrate component by component as you touch them. The @supports pattern makes incremental adoption trivial.

If you skipped the toolkit post: read that one too. Anchor positioning sits on top of :has(), cascade layers, and container queries. The whole modern stack reinforces itself.

What are you going to delete first?

Resources

ReactNext.jsPerformanceWeb DevelopmentBest Practices

30 Days with the React Compiler: What I Stopped Memoizing

9 min read
AIWeb DevelopmentUXForm ValidationJavaScript

Building AI-Powered Form Validation: Beyond Basic RegEx

13 min read
CSSWeb DevelopmentFrontendBest Practices

The Modern CSS Toolkit: What Actually Matters in 2026

10 min read