The Two Pieces: Anchor Positioning + Popover
These are separate specs that work well together:
CSS Anchor Positioning handles the "where does the floating element go?" question. It introduces the anchor() function, anchor-name property, and position-try-fallbacks for automatic repositioning.
Popover API handles the "how does the floating element behave?" question. It gives you top-layer rendering (always on top, no z-index wars), light-dismiss (click outside to close), and proper accessibility semantics.
You can use either one independently, but together they replace the entire Floating UI + React Portal + click-outside-handler + z-index-management stack.
Anchor Positioning: The Basics
The simplest possible example, a tooltip positioned below a button:
/* The anchor (trigger element) */
.trigger {
anchor-name: --my-trigger;
}
/* The positioned element (tooltip) */
.tooltip {
position: fixed;
position-anchor: --my-trigger;
/* Position the tooltip's top-left corner at the anchor's bottom-center */
top: anchor(bottom);
left: anchor(center);
translate: -50% 0; /* Center horizontally */
/* Ensure it doesn't overflow the viewport */
position-try-fallbacks: flip-block;
}
<button class="trigger">Hover me</button>
<div class="tooltip" popover>I'm a tooltip!</div>
The anchor() function takes a side keyword (top, right, bottom, left, center) and returns the corresponding coordinate of the anchor element. No getBoundingClientRect(), no resize observers, no scroll listeners.
The anchor() Function in Detail
The function signature is more powerful than the basic example suggests:
.positioned {
position: fixed;
position-anchor: --my-anchor;
/* Basic: position relative to anchor edges */
top: anchor(bottom); /* align to anchor's bottom edge */
left: anchor(left); /* align to anchor's left edge */
right: anchor(right); /* align to anchor's right edge */
/* With percentage: interpolate between edges */
left: anchor(50%); /* center of anchor (same as anchor(center)) */
top: anchor(25%); /* 25% from top of anchor */
/* Calc works too */
top: calc(anchor(bottom) + 8px); /* 8px gap below anchor */
left: calc(anchor(center) - 20px); /* offset from center */
}
You can also reference a specific anchor inline without position-anchor:
.tooltip {
position: fixed;
top: anchor(--specific-button bottom);
left: anchor(--specific-button center);
}
Production Tooltip Component
A complete tooltip system with arrow, animation, and automatic repositioning:
/* Tooltip trigger */
[data-tooltip-anchor] {
anchor-name: --tooltip-anchor;
}
/* Tooltip container */
.tooltip {
position: fixed;
position-anchor: --tooltip-anchor;
position-visibility: anchors-visible;
/* Default position: below and centered */
top: calc(anchor(bottom) + 8px);
justify-self: anchor-center;
/* Styling */
background: var(--tooltip-bg, #1a1a2e);
color: var(--tooltip-text, #eee);
padding: 8px 12px;
border-radius: 6px;
font-size: 0.875rem;
max-width: 280px;
width: max-content;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.15));
/* Animation */
opacity: 0;
transform: translateY(-4px);
transition: opacity 150ms ease, transform 150ms ease;
/* Fallback positions */
position-try-fallbacks:
--tooltip-above,
--tooltip-left,
--tooltip-right;
}
/* Show state */
.trigger:hover + .tooltip,
.trigger:focus-visible + .tooltip {
opacity: 1;
transform: translateY(0);
}
/* Arrow */
.tooltip::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
translate: -50% 0;
border: 6px solid transparent;
border-bottom-color: var(--tooltip-bg, #1a1a2e);
}
/* Fallback position: above */
@position-try --tooltip-above {
bottom: calc(anchor(top) + 8px);
top: auto;
justify-self: anchor-center;
}
/* Fallback position: left */
@position-try --tooltip-left {
right: calc(anchor(left) + 8px);
top: anchor(center);
left: auto;
translate: 0 -50%;
}
/* Fallback position: right */
@position-try --tooltip-right {
left: calc(anchor(right) + 8px);
top: anchor(center);
translate: 0 -50%;
}
<button class="trigger" data-tooltip-anchor>
Settings
</button>
<div class="tooltip">
Configure your preferences
</div>
When the tooltip would overflow the viewport below the button, the browser automatically tries --tooltip-above, then --tooltip-left, then --tooltip-right. No JavaScript. No resize observer. No scroll handler.
The Popover API: Capabilities and Behavior
The Popover API is not simply display: none toggling. It provides three things that are genuinely hard to build correctly:
1. Top Layer Rendering
Elements with the popover attribute render in the browser's top layer, the same layer used by <dialog> and fullscreen elements. This means no z-index stacking context issues. Your popover will always render above everything else, even if its DOM parent has overflow: hidden or a low z-index.
<div style="overflow: hidden; position: relative; z-index: 1;">
<button popovertarget="my-popup">Open</button>
</div>
<!-- This renders in the top layer, unaffected by the parent's overflow/z-index -->
<div id="my-popup" popover>
I'm free from z-index prison!
</div>
2. Light Dismiss
popover="auto" (the default) gives you automatic light-dismiss. Clicking anywhere outside the popover closes it. Pressing Escape closes it. Opening another auto popover closes the previous one. This is the behavior that every dropdown library reimplements, and the browser does it correctly for all input modalities (mouse, touch, keyboard).
<!-- Auto popover: light-dismiss enabled -->
<button popovertarget="dropdown">Menu</button>
<div id="dropdown" popover="auto">
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<a href="/logout">Logout</a>
</div>
<!-- Manual popover: no light-dismiss, must be closed explicitly -->
<button popovertarget="notification">Show</button>
<div id="notification" popover="manual">
This stays until you close it.
<button popovertarget="notification" popovertargetaction="hide">Close</button>
</div>
3. Accessibility Baked In
The popover API handles focus management and ARIA attributes automatically. When a popover opens, it becomes accessible to assistive technologies. The popovertarget button automatically gets aria-expanded behavior.
Building a Complete Dropdown Menu
A production-grade dropdown menu using both APIs together:
<nav class="main-nav">
<button
class="nav-trigger"
popovertarget="products-menu"
style="anchor-name: --products-trigger"
>
Products ▾
</button>
<div
id="products-menu"
class="dropdown-menu"
popover="auto"
style="position-anchor: --products-trigger"
>
<a href="/products/analytics" class="dropdown-item">
<span class="item-icon">📊</span>
<div>
<strong>Analytics</strong>
<small>Track user behavior</small>
</div>
</a>
<a href="/products/monitoring" class="dropdown-item">
<span class="item-icon">🔍</span>
<div>
<strong>Monitoring</strong>
<small>Real-time alerting</small>
</div>
</a>
<hr class="dropdown-divider" />
<a href="/products/all" class="dropdown-item">
View all products →
</a>
</div>
</nav>
.dropdown-menu {
position: fixed;
position-anchor: --products-trigger;
/* Position below trigger, aligned to left edge */
top: calc(anchor(bottom) + 4px);
left: anchor(left);
/* Sizing */
min-width: anchor-size(width); /* At least as wide as the trigger */
max-width: 320px;
/* Styling */
background: white;
border: 1px solid #e2e2e2;
border-radius: 8px;
padding: 4px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
/* Animation */
opacity: 0;
transform: translateY(-8px) scale(0.96);
transition: opacity 200ms ease,
transform 200ms ease,
overlay 200ms ease allow-discrete,
display 200ms ease allow-discrete;
/* Fallback: flip above if no room below */
position-try-fallbacks: flip-block;
}
/* Open state */
.dropdown-menu:popover-open {
opacity: 1;
transform: translateY(0) scale(1);
}
/* Starting style for entry animation */
@starting-style {
.dropdown-menu:popover-open {
opacity: 0;
transform: translateY(-8px) scale(0.96);
}
}
.dropdown-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
border-radius: 6px;
text-decoration: none;
color: inherit;
transition: background 100ms ease;
}
.dropdown-item:hover {
background: #f5f5f5;
}
.dropdown-divider {
border: none;
border-top: 1px solid #e8e8e8;
margin: 4px 0;
}
Zero JavaScript. The button toggles the popover. The popover positions itself relative to the button. Clicking outside closes it. If there's no room below, it flips above. The entry animation uses @starting-style for proper CSS animation of display: none to display: block.
The anchor-size() Function
One underappreciated feature is anchor-size(), which allows reading the dimensions of the anchor element:
.dropdown {
/* Make dropdown at least as wide as the trigger */
min-width: anchor-size(width);
/* Or match it exactly */
width: anchor-size(width);
/* You can also read height */
max-height: calc(100vh - anchor-size(height) - 16px);
}
This is well suited for autocomplete dropdowns that should match the width of the input field, or for mega-menus that span the width of their trigger nav item.
position-try-fallbacks: Automatic Repositioning
The fallback system is what makes anchor positioning production-ready. Without it, JavaScript would be needed to detect overflow and reposition, which is exactly what Floating UI does.
Built-in Fallback Keywords
.tooltip {
position-try-fallbacks:
flip-block, /* Try flipping top↔bottom */
flip-inline, /* Try flipping left↔right */
flip-block flip-inline; /* Try flipping both */
}
Custom Fallback Positions
@position-try --above-right {
bottom: anchor(top);
left: anchor(right);
top: auto;
right: auto;
}
@position-try --below-left {
top: anchor(bottom);
right: anchor(left);
left: auto;
}
.menu {
/* Try default position, then above-right, then below-left */
position-try-fallbacks: --above-right, --below-left;
}
The browser evaluates each fallback in order and uses the first one that fits within the viewport. If none fit, it uses the last one (or the original position, depending on the position-try-order property).
Nested Popovers and Popover Stacking
The Popover API handles nesting correctly, which is something that took years to get right in JavaScript libraries:
<button popovertarget="menu-1" style="anchor-name: --btn-1">File</button>
<div id="menu-1" popover="auto" style="position-anchor: --btn-1">
<button popovertarget="submenu-1" style="anchor-name: --sub-btn-1">
Export →
</button>
<div id="submenu-1" popover="auto" style="position-anchor: --sub-btn-1">
<button>As PDF</button>
<button>As CSV</button>
<button>As JSON</button>
</div>
</div>
When submenu-1 is open and you click outside both popovers, both close. If you click on menu-1 (but outside submenu-1), only the submenu closes. The browser maintains a "popover stack" and handles dismissal in the correct order.
Browser Support Status
Current support as of early 2026:
| Feature | Chrome | Firefox | Safari |
|---|---|---|---|
| Popover API | 114+ | 125+ | 17+ |
anchor-name / position-anchor |
125+ | Behind flag | In development |
anchor() function |
125+ | Behind flag | In development |
position-try-fallbacks |
125+ | Behind flag | In development |
anchor-size() |
125+ | Behind flag | In development |
@position-try |
125+ | Behind flag | In development |
position-visibility |
131+ | No | No |
The Popover API has broad support. Anchor positioning is Chrome/Edge only in production. Firefox is close (behind a flag since 127), and Safari is actively working on it.
Progressive Enhancement Strategy
/* Base: works everywhere */
.tooltip {
position: fixed;
/* JS-based positioning as fallback */
}
/* Enhanced: anchor positioning where supported */
@supports (anchor-name: --test) {
.tooltip {
position-anchor: --trigger;
top: calc(anchor(bottom) + 8px);
justify-self: anchor-center;
position-try-fallbacks: flip-block;
}
}
// Only load Floating UI if anchor positioning isn't supported
if (!CSS.supports('anchor-name', '--test')) {
import('floating-ui').then(({ computePosition, flip, offset }) => {
// Fallback positioning logic
});
}
This way, 70%+ of users (Chrome/Edge) get the native experience with zero JavaScript, and the rest get the Floating UI fallback.
Implications for UI Libraries
Every major component library ships some form of floating UI positioning:
- Radix UI uses Floating UI internally
- shadcn/ui inherits this from Radix
- Headless UI (Tailwind Labs) uses its own positioning
- Ant Design, MUI, Chakra, all have custom positioning logic
As anchor positioning gains browser support, these libraries will progressively adopt it. Some already have experimental flags. The end state is that positioning logic moves from the JavaScript bundle to the browser engine, reducing both bundle size and runtime computation.
For new projects targeting only Chromium browsers (Electron apps, internal tools, Chrome extensions), anchor positioning can be used today with zero fallback needed. For public-facing websites, the progressive enhancement approach shown above is the pragmatic path.
The web platform is finally catching up to what native UI frameworks have done for decades, letting the layout engine handle element positioning. It took longer than it should have, but the capability is here now.