Introduction
Every frontend developer has wrestled with the challenge of positioning floating UI elements. Tooltips that get clipped by viewport edges, dropdown menus that overlap their triggers, and popovers that refuse to stay in place — these are universal frustrations that have historically required JavaScript libraries to solve. CSS Anchor Positioning is the browser-native answer to this problem, offering a declarative approach that eliminates the need for runtime positioning calculations entirely.
With CSS Anchor Positioning, you can attach any element to any other element using simple CSS declarations. The browser handles all the complex geometry: calculating positions, detecting viewport overflow, and automatically flipping elements to alternative positions when space is limited. This means smaller bundle sizes, fewer runtime dependencies, and better performance for your floating UI components.
This guide walks through the three most common floating UI patterns — tooltips, popovers, and dropdowns — with production-ready implementations using CSS Anchor Positioning. We'll cover the core API, fallback strategies, and real-world patterns you can adopt immediately.
Understanding Anchor Positioning: Core Concepts
The CSS Anchor Positioning API revolves around three key primitives. First, the anchor-name property declares an element as an anchor — a reference point that other elements can attach to. Second, the position-anchor property on a positioned element establishes the connection to a named anchor. Third, the anchor() function and position-area property control exactly where the positioned element sits relative to the anchor.
/* Step 1: Declare the anchor */
.button {
anchor-name: --action-button;
}
/* Step 2: Position relative to the anchor */
.tooltip {
position: fixed;
position-anchor: --action-button;
position-area: top;
}The anchor() function provides pixel-level control by returning the coordinate of a specific edge on the anchor element. You can use it with top, right, bottom, left, or center keywords to position elements precisely where you need them.
The position-area property offers a higher-level abstraction. It divides the space around the anchor into a 3×3 grid and lets you specify placement using intuitive keywords like top, bottom, left, right, and combinations like top right. This is often simpler than using individual anchor() calls for each axis.
Fallback Positioning
When a positioned element would overflow the viewport, the position-try-fallbacks property defines alternative positions the browser should try. The browser selects the first fallback that keeps the element fully visible, eliminating the need for manual collision detection logic.
.dropdown {
position-try-fallbacks: --above, --left, --right;
}
@position-try --above {
position-area: top;
margin-block: 0 4px;
}Scoping and Nesting
Anchor names are scoped to their containing block by default. This means components can reuse the same anchor names without conflicts — a critical feature for design systems and component libraries. The upcoming anchor-scope property will provide even finer-grained control.
Architecture and Design Patterns
Pattern 1: Informational Tooltips
Tooltips are the simplest floating UI pattern — they display supplementary information on hover or focus and disappear when the interaction ends. The architecture requires an anchor trigger, a tooltip element, and CSS that handles positioning and visibility transitions.
The key architectural decision is whether to use sibling adjacency (+ or ~ selectors) or the Popover API for show/hide behavior. For simple tooltips, sibling adjacency with CSS transitions is sufficient and avoids any JavaScript.
Pattern 2: Interactive Popovers
Popovers contain interactive content like forms, menus, or detailed information panels. They require the HTML Popover API for proper semantics — automatic focus management, light-dismiss behavior (click outside to close), and proper stacking context. The architecture combines popover attribute semantics with anchor positioning for placement.
Pattern 3: Command Dropdowns
Dropdown menus for actions (edit, delete, share) need to appear below their trigger, handle keyboard navigation, and support nested submenus. The architecture involves a trigger button, a menu container with role="menu", and anchor positioning that ensures the menu stays visible even when the trigger is near viewport edges.
Pattern 4: Select/Multi-Select Components
Custom select components are the most complex pattern. They need to match the width of the trigger, handle scrollable option lists, and support search/filter input. Anchor positioning handles the alignment while the component logic manages the option list state.
Step-by-Step Implementation
Building a Tooltip System
Start with semantic HTML that provides accessibility information:
<span class="tooltip-container">
<button class="tooltip-trigger" aria-describedby="save-tip">
Save
</button>
<span id="save-tip" role="tooltip" class="tooltip">
Save your current progress (Ctrl+S)
</span>
</span>Implement the anchor positioning with automatic fallbacks:
.tooltip-container {
position: relative;
display: inline-flex;
}
.tooltip-trigger {
anchor-name: --tip-anchor;
}
.tooltip {
position: fixed;
position-anchor: --tip-anchor;
position-area: top;
margin-block-end: 8px;
width: max-content;
max-width: 200px;
padding: 6px 10px;
font-size: 13px;
background: #333;
color: #fff;
border-radius: 4px;
opacity: 0;
visibility: hidden;
transition: opacity 0.15s, visibility 0.15s;
position-try-fallbacks: --tip-below, --tip-left, --tip-right;
}
.tooltip-trigger:hover + .tooltip,
.tooltip-trigger:focus-visible + .tooltip {
opacity: 1;
visibility: visible;
}
@position-try --tip-below {
position-area: bottom;
margin-block-end: 0;
margin-block-start: 8px;
}
@position-try --tip-left {
position-area: left;
margin-inline-end: 8px;
margin-block: 0;
}
@position-try --tip-right {
position-area: right;
margin-inline-start: 8px;
margin-block: 0;
}Building a Popover Menu
Combine anchor positioning with the HTML Popover API for an accessible dropdown:
<button popovertarget="action-menu" class="menu-trigger">
Actions â–¾
</button>
<div id="action-menu" popover class="menu">
<button class="menu-item">Edit</button>
<button class="menu-item">Duplicate</button>
<button class="menu-item">Archive</button>
<hr>
<button class="menu-item danger">Delete</button>
</div>.menu-trigger {
anchor-name: --menu-anchor;
}
.menu {
position: fixed;
position-anchor: --menu-anchor;
position-area: block-end span-inline-end;
margin-block-start: 4px;
width: max-content;
min-width: 160px;
padding: 4px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: white;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
position-try-fallbacks: --menu-above, --menu-left;
}
@position-try --menu-above {
position-area: block-start span-inline-end;
margin-block-start: 0;
margin-block-end: 4px;
}
@position-try --menu-left {
position-area: inline-start span-block-end;
margin-inline-end: 4px;
margin-inline-start: 0;
}
.menu-item {
display: block;
width: 100%;
padding: 8px 12px;
border: none;
background: none;
text-align: left;
cursor: pointer;
border-radius: 4px;
}
.menu-item:hover {
background: #f0f0f0;
}Building a Select Dropdown
For a custom select component with search:
<div class="select-container">
<button class="select-trigger" popovertarget="country-select">
Select country...
</button>
<div id="country-select" popover class="select-dropdown">
<input type="search" placeholder="Search countries..." class="select-search">
<div class="select-options">
<button class="select-option">United States</button>
<button class="select-option">United Kingdom</button>
<button class="select-option">Canada</button>
<!-- More options -->
</div>
</div>
</div>.select-container {
position: relative;
}
.select-trigger {
anchor-name: --select-anchor;
width: 240px;
padding: 8px 12px;
text-align: left;
border: 1px solid #ccc;
border-radius: 6px;
}
.select-dropdown {
position: fixed;
position-anchor: --select-anchor;
position-area: block-end;
margin-block-start: 4px;
width: anchor-size(width);
max-height: 300px;
padding: 4px;
border: 1px solid #ddd;
border-radius: 6px;
background: white;
overflow-y: auto;
position-try-fallbacks: --select-above;
}
@position-try --select-above {
position-area: block-start;
margin-block-start: 0;
margin-block-end: 4px;
}Real-World Use Cases
Use Case 1: Rich Text Editor Toolbar
In a rich text editor, a formatting toolbar appears above selected text when the user makes a selection. The toolbar needs to follow the selection as the user changes it, stay within the viewport bounds, and disappear when the selection is cleared. Anchor positioning handles the spatial relationship while the Popover API manages the visibility lifecycle.
Use Case 2: Help Center Knowledge Base
A help center with expandable FAQ items benefits from contextual tooltips that show related articles when users hover over technical terms. These tooltips need to appear near the hovered word, handle long content gracefully, and flip to alternative positions when near viewport edges.
Use Case 3: E-Commerce Quick View
Product listing pages often include a "Quick View" popover that shows product details without navigating away. The popover needs to appear near the trigger card, contain interactive elements (size selector, add to cart), and dismiss cleanly when the user clicks outside.
Use Case 4: Dashboard Date Range Picker
Dashboard widgets frequently use date range pickers in popovers. The picker needs to appear below the trigger input, handle month navigation within the popover, and stay aligned even when the dashboard layout changes responsively.
Best Practices for Production
-
Use
position-areaoveranchor()for common patterns — The grid-based shorthand is more readable and less error-prone than calculating individual edge coordinates. Reserveanchor()for cases requiring pixel-precise positioning. -
Define at least three fallback positions — Cover the primary direction plus two alternatives. For tooltips above an anchor, define below, left, and right fallbacks. This handles the vast majority of viewport edge cases.
-
Set
position-visibility: anchor-visible— This ensures tooltips and popovers automatically hide when their anchor scrolls out of the viewport, preventing orphaned floating elements. -
Use
width: anchor-size(width)for select dropdowns — This makes the dropdown match the width of its trigger anchor, creating a polished, aligned appearance without manual width calculations. -
Combine anchor positioning with the Popover API — The Popover API provides accessibility features (focus trapping, light dismiss) that anchor positioning alone doesn't handle. Use both together for production-ready floating UI.
-
Test on mobile viewports — Anchor positioning can produce unexpected results on very small screens where most positions overflow. Add a mobile-specific fallback that centers the floating element.
-
Avoid animating layout properties — Use
opacityandtransformfor show/hide animations instead oftop/leftchanges. This triggers compositing rather than layout, resulting in smoother animations. -
Use
@layerfor z-index management — Floating elements need higher stacking order than page content. Define a dedicated@layerfor all floating UI to ensure consistent z-index management.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
Not setting position: fixed | Element ignores anchor positioning | Anchored elements require position: fixed or position: absolute |
| Same anchor name across unrelated components | Elements attach to wrong anchor | Use component-scoped anchor names like --card-menu-anchor |
| No fallback positions defined | Floating element overflows viewport | Always define position-try-fallbacks with alternatives |
Forgetting width: max-content on tooltips | Text wraps to single-character width | Set width: max-content with a max-width constraint |
Overriding [popover] display property | Popover API behavior breaks | Apply layout styles to children, not the popover element itself |
Anchor element has overflow: hidden ancestor | Positioned element gets clipped | Ensure no ancestor clips position: fixed elements |
Performance Optimization
CSS Anchor Positioning executes positioning calculations during the browser's layout phase, which means it runs on the compositor thread for position: fixed elements. This is significantly faster than JavaScript-based positioning that runs on the main thread and triggers layout thrashing.
For pages with many anchored elements (like a table with per-row action menus), minimize the number of @position-try definitions. Each fallback rule adds to the layout calculation cost. Create shared fallback utility classes that all elements in the same context can reference.
/* Shared fallback strategy for all table menus */
[data-popover-type="table-action"] {
position-try-fallbacks: --action-above, --action-left;
}Avoid triggering unnecessary recalculations by not changing anchor names dynamically. If you must swap anchors (e.g., for different screen sizes), use media queries rather than JavaScript class toggles.
Comparison with Alternatives
| Feature | CSS Anchor Positioning | Floating UI | Headless UI | Radix Popover |
|---|---|---|---|---|
| Bundle size impact | 0 KB (native) | ~5 KB | ~8 KB | ~12 KB |
| JavaScript dependency | None | Required | Required | Required |
| Viewport collision handling | Automatic | Configurable middleware | Built-in | Built-in |
| Accessibility | Via Popover API | Manual implementation | Built-in ARIA | Built-in ARIA |
| Animation support | CSS only | JS + CSS | JS + CSS | JS + CSS |
| Browser support | Chrome/Edge 125+ | All modern | All modern | All modern |
| Learning curve | Low (CSS-only) | Medium | Medium | Low |
Advanced Patterns
Nested Dropdowns with Anchor Chains
For nested submenus, each level declares its own anchor and positions relative to its parent menu item. The anchor chain creates a cascade of positioned elements that all respond to viewport changes together.
.menu-item {
anchor-name: --menu-item;
}
.submenu {
position: fixed;
position-anchor: --menu-item;
position-area: inline-end span-block-start;
position-try-fallbacks: --submenu-left;
}Context Menus at Cursor Position
Context menus require positioning at the cursor rather than at a fixed anchor. Create a virtual anchor by positioning a zero-size element at the click coordinates, then attach the menu to that anchor:
document.addEventListener('contextmenu', (e) => {
const anchor = document.getElementById('cursor-anchor');
anchor.style.left = `${e.clientX}px`;
anchor.style.top = `${e.clientY}px`;
document.getElementById('context-menu').showPopover();
});#cursor-anchor {
position: fixed;
anchor-name: --cursor;
width: 0;
height: 0;
}
#context-menu {
position: fixed;
position-anchor: --cursor;
position-area: bottom right;
}Responsive Anchor Sizing
Use anchor-size() to make positioned elements match their anchor's dimensions:
.autocomplete-dropdown {
width: anchor-size(width);
max-height: anchor-size(height);
/* Dropdown width matches input width */
}Testing Strategies
Test anchor-positioned components with both unit tests for behavior and visual regression tests for positioning accuracy:
import { test, expect } from '@playwright/test';
test('dropdown flips above trigger when near viewport bottom', async ({ page }) => {
await page.goto('/test-page');
// Scroll trigger near bottom of viewport
const trigger = page.locator('[popovertarget="menu"]');
await page.evaluate(() => {
document.querySelector('[popovertarget="menu"]')
.scrollIntoView({ block: 'end' });
});
await trigger.click();
const menu = page.locator('#menu');
const menuBox = await menu.boundingBox();
const triggerBox = await trigger.boundingBox();
// Menu should be above the trigger
expect(menuBox.y + menuBox.height).toBeLessThanOrEqual(triggerBox.y);
});Accessibility Considerations
When implementing anchor-positioned elements, accessibility must be a primary concern. Tooltips should use role="tooltip" and be associated with their trigger using aria-describedby. Popovers and dropdowns need proper aria-expanded and aria-haspopup attributes on their trigger elements. Ensure that keyboard navigation works correctly — users should be able to open, navigate within, and close floating elements using only the keyboard. The Popover API handles focus management automatically when used with anchor positioning, but custom implementations need manual focus trapping. Test with screen readers to verify that floating content is announced correctly and that focus order is logical.
Future Outlook
CSS Anchor Positioning is rapidly gaining cross-browser support. Firefox has it in development, and Safari's WebKit team has expressed interest. The anchor-scope property, currently in the spec, will simplify component-level anchor management by providing explicit scope boundaries. Combined with the Popover API and View Transitions, these APIs are building toward a future where complex interactive UI can be built entirely with HTML and CSS.
Conclusion
CSS Anchor Positioning transforms how we build floating UI on the web. The three core patterns — tooltips, popovers, and dropdowns — cover the vast majority of floating element use cases. The key takeaways are:
- Declare anchors with
anchor-nameand attach positioned elements withposition-anchorfor declarative, CSS-only positioning. - Use
position-areafor intuitive placement — the grid-based shorthand handles most positioning needs without manual coordinate calculations. - Always define
position-try-fallbacksto ensure floating elements remain visible regardless of their position in the viewport. - Combine with the Popover API for accessible, interactive floating UI that handles focus management and light-dismiss automatically.
- Use
anchor-size()for responsive sizing to match dropdown widths to their triggers without JavaScript.
Adopt anchor positioning incrementally — start with tooltips, then migrate dropdowns and popovers. The CSS-only approach reduces bundle size, improves performance, and simplifies your codebase.