MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

CSS Anchor Positioning: Tooltips, Popovers, and Dropdowns

Position elements relative to anchors: tooltip, popover, and dropdown patterns without JS.

CSSAnchor PositioningFrontendUI

By MinhVo

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.

CSS Anchor Positioning enables precise element positioning

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.

Positioning concepts and fallback strategies

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;
}

Building dropdown and select components

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

  1. Use position-area over anchor() for common patterns — The grid-based shorthand is more readable and less error-prone than calculating individual edge coordinates. Reserve anchor() for cases requiring pixel-precise positioning.

  2. 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.

  3. Set position-visibility: anchor-visible — This ensures tooltips and popovers automatically hide when their anchor scrolls out of the viewport, preventing orphaned floating elements.

  4. 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.

  5. 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.

  6. 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.

  7. Avoid animating layout properties — Use opacity and transform for show/hide animations instead of top/left changes. This triggers compositing rather than layout, resulting in smoother animations.

  8. Use @layer for z-index management — Floating elements need higher stacking order than page content. Define a dedicated @layer for all floating UI to ensure consistent z-index management.

Common Pitfalls and Solutions

PitfallImpactSolution
Not setting position: fixedElement ignores anchor positioningAnchored elements require position: fixed or position: absolute
Same anchor name across unrelated componentsElements attach to wrong anchorUse component-scoped anchor names like --card-menu-anchor
No fallback positions definedFloating element overflows viewportAlways define position-try-fallbacks with alternatives
Forgetting width: max-content on tooltipsText wraps to single-character widthSet width: max-content with a max-width constraint
Overriding [popover] display propertyPopover API behavior breaksApply layout styles to children, not the popover element itself
Anchor element has overflow: hidden ancestorPositioned element gets clippedEnsure 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

FeatureCSS Anchor PositioningFloating UIHeadless UIRadix Popover
Bundle size impact0 KB (native)~5 KB~8 KB~12 KB
JavaScript dependencyNoneRequiredRequiredRequired
Viewport collision handlingAutomaticConfigurable middlewareBuilt-inBuilt-in
AccessibilityVia Popover APIManual implementationBuilt-in ARIABuilt-in ARIA
Animation supportCSS onlyJS + CSSJS + CSSJS + CSS
Browser supportChrome/Edge 125+All modernAll modernAll modern
Learning curveLow (CSS-only)MediumMediumLow

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:

  1. Declare anchors with anchor-name and attach positioned elements with position-anchor for declarative, CSS-only positioning.
  2. Use position-area for intuitive placement — the grid-based shorthand handles most positioning needs without manual coordinate calculations.
  3. Always define position-try-fallbacks to ensure floating elements remain visible regardless of their position in the viewport.
  4. Combine with the Popover API for accessible, interactive floating UI that handles focus management and light-dismiss automatically.
  5. 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.