CSS Anchor Positioning: Anchoring Popovers without Popper.js
Prologue: The Infinite War of Pixel Calculations
If I had to pick the single most frustrating component to build in frontend development, it would undoubtedly be floating user interfaces: tooltips, dropdowns, and popovers.
Similar to writing side-notes on complex historical texts during my college days, placing a small floating information box directly adjacent to a button seems simple on paper, yet it is a notorious hotbed for UI bugs in practice.
Historically, configuring these positions was a headache:
- Using
position: absolute: Discarded because if any parent element possessedoverflow: hidden, the tooltip was clipped. - Using
position: fixedand tracking coordinates dynamically: Calculated using JavaScript'sgetBoundingClientRect(), which often resulted in lagging tooltips during scroll, or layout reflow performance issues. - Importing large third-party libraries like
Popper.jsorFloating UI.
While importing libraries solved the problem, it felt like an anti-pattern to load several kilobytes of JavaScript and wrap basic UI markup inside complex React hooks just to display a simple tooltip.
When CSS Anchor Positioning API entered the modern web standards, I realized we could finally solve this layout challenge natively in CSS without any JavaScript helper code.
Concept: Binding Elements Purely in CSS
The core design principle of CSS Anchor Positioning is that the browser's layout engine dynamically tracks and binds the geometric coordinates between two elements.
Previously, if an anchor (like a button trigger) and the target (the tooltip popover) were located in entirely different branches of the DOM tree, there was no way to share coordinates purely within CSS. Anchor Positioning resolves this by mapping them logically through CSS identifiers.
/* 1. Label the anchor element */
.anchor-button {
anchor-name: --my-anchor;
}
/* 2. Bind the target to the anchor */
.tooltip-popover {
position: absolute;
position-anchor: --my-anchor;
top: anchor(bottom); /* Position right below the anchor */
left: anchor(center); /* Center horizontally with the anchor */
}
Testing this setup for the first time felt incredibly liberating:
- No JavaScript event listeners are required.
- The browser adjusts positions on the fly at rendering-frame speed during scroll or window resize events.
- Parent
overflow: hiddenproperties no longer clip the floating element.
Deep Dive: Key Specs of Native Anchor Positioning
To adopt CSS Anchor Positioning in production, three key concepts and properties need to be understood.
1. Coordinate Mapping with anchor()
The anchor() function acts as a coordinate resolver, returning the exact physical coordinate (top, bottom, left, right, center, etc.) of the reference element.
.dropdown-menu {
position: absolute;
position-anchor: --menu-trigger;
/* Match the top of the dropdown with the bottom of the trigger */
top: anchor(bottom);
/* Match the left edge of the dropdown with the left edge of the trigger */
left: anchor(left);
}
It also supports percentage offsets, like anchor(left 20%), giving developers precise control over spacing.
2. Space Detection with position-try-options
One of the hardest edges to program in floating UI design is boundary collision. If a tooltip is programmed to display underneath a button, but the button sits at the very bottom of the viewport, the tooltip overflows and becomes hidden.
Libraries like Popper.js calculated viewport margins and automatically flipped the tooltip upwards. CSS now natively handles this using position-try-options.
.tooltip {
position: absolute;
position-anchor: --tooltip-trigger;
top: anchor(bottom);
left: anchor(center);
/* Automatically flip to the top if bottom space is restricted */
position-try-options: flip-block;
}
The value flip-block represents Y-axis flipping (bottom <-> top), while flip-inline targets the X-axis (left <-> right). The browser pre-calculates the layout margins and flips the placement instantaneously.
3. Integration with the Popover API
Anchor Positioning works in tandem with the native HTML5 Popover API (popover attribute). The Popover API displays elements in the Top Layer, effectively rendering them above all other DOM elements and eliminating z-index styling conflicts.
<!-- HTML Structure -->
<button id="my-btn" class="trigger">Open Menu</button>
<div id="my-menu" popover class="menu-content">
<ul>
<li>Settings</li>
<li>Logout</li>
</ul>
</div>
/* CSS Configurations */
.trigger {
anchor-name: --my-btn-anchor;
}
.menu-content {
/* Popover opens in the Top Layer, with position linked to the anchor */
position-anchor: --my-btn-anchor;
top: anchor(bottom);
left: anchor(left);
margin: 0; /* Erase default popover margins */
}
Application: Refactoring a User Profile Dropdown
I applied this specification to refactor the profile dropdown menu in my side project.
The legacy code relied on a useRef binding to the button, combined with window scroll event listeners in a useEffect hook, which frequently slowed down rendering speeds.
// Refactored React component with native CSS Anchor Positioning
import { useId } from 'react';
function UserProfileDropdown() {
const uniqueId = useId();
// Ensure unique anchor names using React IDs
const anchorName = `--profile-btn-${uniqueId.replace(/:/g, '')}`;
return (
<div className="relative">
{/* Anchor Trigger Button */}
<button
popovertarget="profile-menu"
style={{ anchorName } as React.CSSProperties}
className="w-10 h-10 rounded-full bg-slate-200"
>
👤
</button>
{/* Popover Menu Content */}
<div
id="profile-menu"
popover="auto"
style={{ positionAnchor: anchorName } as React.CSSProperties}
className="absolute p-4 bg-white rounded-lg shadow-xl border border-slate-100 hidden-popover"
>
<p className="font-bold text-slate-800">John Doe</p>
<hr className="my-2 border-slate-100" />
<button className="text-sm text-red-500">Logout</button>
</div>
</div>
);
}
/* Globals CSS configurations */
[popover] {
border: none;
padding: 0;
overflow: visible;
/* Link position anchor */
top: anchor(bottom);
left: anchor(right);
transform: translate(-100%, 8px); /* Right align with vertical margin offset */
position-try-options: flip-block;
}
The results of this refactoring were outstanding:
- React's
isOpentoggle state (useState) was eliminated. The native HTMLpopoverattribute manages toggle state automatically. - All JavaScript-based layout calculations and coordinates were deleted.
- Even during fast scrolling, the browser leverages GPU rendering to align the dropdown perfectly under the button, delivering a smooth UI experience.
Summary: The Benefits of Smart Runtimes
The history of software engineering is a process of offloading tasks originally managed in user space (JavaScript runtimes) down to lower-level runtime platforms (browser rendering engines). Just as CSS Grid made complex table/float hacks obsolete, Anchor Positioning represents the logical conclusion of dropdown and modal styling.
By writing declarative HTML and CSS without importing external libraries, we can create top-layer popovers that require zero JavaScript code.
While browser compatibility polyfills might still be necessary for older targets, this specification stands as a testament to the power of modern web standards, making our lives as developers much easier.