CSS Anchor Positioning in 2026: Building Menus, Callouts, and Popovers That Stay Attached

Layout Systems
Web page showing CSS anchor-positioned callouts and popovers attached to navigation menu items

Positioning an element relative to another element has been one of the most awkward problems in CSS for decades. You want a tooltip above a button, a callout next to a form field, a dropdown menu below a navigation link. CSS gives you position: absolute relative to the nearest positioned ancestor—not relative to an arbitrary element elsewhere in the DOM. So you either wrap things in positioned containers (which distorts your layout), calculate offsets in JavaScript (which requires resize observers, scroll handlers, and constant recalculation), or use a library like Floating UI that does all of that calculation for you.

CSS anchor positioning eliminates this entire category of workaround. You declare an element as an anchor, then position another element relative to that anchor using the anchor() function. The browser handles the geometry. No JavaScript. No wrapper elements. No offset calculations. In 2026, this is finally usable in production across all major browsers.

The Core Syntax

Anchor positioning requires two things: declaring an anchor and referencing it.

Declaring an Anchor

.trigger-button {
  anchor-name: --menu-trigger;
}

The anchor-name property accepts a dashed-ident (like custom properties). Any element can be an anchor. The name must be unique on the page—if two elements share the same anchor-name, the last one in document order wins.

Positioning Relative to an Anchor

.dropdown-menu {
  position: fixed;
  position-anchor: --menu-trigger;
  top: anchor(bottom);
  left: anchor(left);
}

position-anchor specifies which anchor this element attaches to. The anchor() function references a specific edge of that anchor element. anchor(bottom) resolves to the bottom edge’s position, anchor(left) to the left edge. The dropdown is now positioned with its top-left corner at the bottom-left corner of the trigger button.

The positioned element must use position: fixed or position: absolute. Fixed positioning is generally preferable because the element sits in the top layer, above all other content, without z-index complications.

A Complete Minimal Example

<button class="trigger" id="info-btn">More Info</button>
<div class="callout" id="info-callout">
  Additional details about this feature.
</div>
#info-btn {
  anchor-name: --info-anchor;
}

#info-callout {
  position: fixed;
  position-anchor: --info-anchor;
  top: anchor(bottom);
  left: anchor(center);
  translate: -50% 0;
  margin-top: 0.5rem;

  background: #1a1a2e;
  color: #fff;
  padding: 0.75rem 1rem;
  border-radius: 0.5rem;
  max-width: 300px;
}

The callout sits below the button, horizontally centred (using anchor(center) plus translate: -50%). When the button moves—because the page scrolls, the layout changes, or the viewport resizes—the callout follows automatically. No JavaScript.

Building an Attached Dropdown Menu

Dropdown menus are the most common use case for anchor positioning. Previously, you needed either a wrapper <div> with position: relative around the trigger and menu, or JavaScript to calculate the menu’s position. With anchor positioning, the menu attaches to its trigger regardless of DOM structure.

<nav aria-label="Main">
  <ul class="nav-list">
    <li>
      <button
        class="nav-trigger"
        popovertarget="services-menu"
        aria-expanded="false"
        aria-haspopup="true"
        style="anchor-name: --services-trigger"
      >Services</button>
      <ul id="services-menu" popover role="menu" class="nav-dropdown">
        <li role="none"><a role="menuitem" href="/design/">Design</a></li>
        <li role="none"><a role="menuitem" href="/hosting/">Hosting</a></li>
        <li role="none"><a role="menuitem" href="/seo/">SEO</a></li>
      </ul>
    </li>
    <li><a href="/about/">About</a></li>
    <li><a href="/contact/">Contact</a></li>
  </ul>
</nav>
.nav-dropdown {
  position: fixed;
  position-anchor: --services-trigger;
  top: anchor(bottom);
  left: anchor(left);
  margin: 0;
  padding: 0.5rem 0;
  border: 1px solid #ddd;
  border-radius: 0.5rem;
  box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
  min-width: 180px;
}

This combines the Popover API for toggle behaviour and accessibility with anchor positioning for placement. The popover handles show/hide, light-dismiss, and Escape-to-close. The anchor positioning handles where the menu appears. Each API does what it is best at.

For keyboard navigation and ARIA roles, you still need the same patterns—aria-expanded, arrow key handlers, focus management. Anchor positioning is purely visual. It does not change the interaction model.

Tooltips and Callouts That Stay Attached

The tooltip use case is where anchor positioning shines brightest. Traditional CSS tooltips relied on adjacent sibling selectors and absolute positioning within a wrapper, or on JavaScript libraries that tracked element positions. Anchor positioning decouples the tooltip from any specific DOM relationship.

.help-icon {
  anchor-name: --help-icon;
}

.help-tooltip {
  position: fixed;
  position-anchor: --help-icon;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
  margin-bottom: 0.5rem;

  background: #333;
  color: #fff;
  padding: 0.5rem 0.75rem;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  white-space: nowrap;
  pointer-events: none;

  opacity: 0;
  transition: opacity 0.15s ease;
}

.help-icon:hover + .help-tooltip,
.help-icon:focus-visible + .help-tooltip {
  opacity: 1;
}

The tooltip appears above the icon, centred horizontally. On hover or keyboard focus, it fades in. No JavaScript, no position calculation, no resize observers. The tooltip is positioned in the top layer (via position: fixed) so it cannot be clipped by overflow: hidden on ancestor elements—a problem that plagued the old position: absolute tooltip approach.

Adding an Arrow

The CSS triangle technique still works for tooltip arrows, but anchor positioning makes placement precise:

.help-tooltip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  translate: -50% 0;
  border: 6px solid transparent;
  border-top-color: #333;
}

The arrow sits at the bottom of the tooltip, pointing down toward the anchor element. Because the tooltip itself is anchor-positioned, the arrow stays aligned automatically.

Fallback Positioning with position-try

What happens when the anchored element would overflow the viewport? A tooltip positioned above a button near the top of the page has nowhere to go. In JavaScript-based solutions, this requires collision detection and repositioning logic. CSS anchor positioning handles this natively with position-try-fallbacks.

.tooltip {
  position: fixed;
  position-anchor: --trigger;

  /* Primary position: above the anchor */
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
  margin-bottom: 0.5rem;

  /* Fallback: try below the anchor if above doesn't fit */
  position-try-fallbacks: flip-block;
}

flip-block tells the browser: if the primary position causes overflow in the block direction (vertically), flip it. A tooltip that would go above the viewport instead appears below the anchor. Other built-in fallback keywords include:

  • flip-inline — flip horizontally
  • flip-block — flip vertically
  • flip-start — flip in the start direction (logical)

You can also define custom fallback positions using @position-try:

@position-try --below-right {
  top: anchor(bottom);
  right: anchor(right);
  margin-top: 0.5rem;
}

.tooltip {
  position: fixed;
  position-anchor: --trigger;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
  margin-bottom: 0.5rem;

  position-try-fallbacks: flip-block, --below-right;
}

The browser tries the primary position first, then flip-block, then the custom --below-right position. The first one that fits without overflow wins. This is collision detection without a single line of JavaScript.

Integration with the Popover API

Anchor positioning and the Popover API are designed to work together. Popovers already render in the top layer and handle show/hide semantics. Anchor positioning handles placement. The combination covers most tooltip, callout, and menu patterns:

<button popovertarget="details-popup" style="anchor-name: --details-btn">
  Details
</button>
<div id="details-popup" popover style="position-anchor: --details-btn">
  <p>Extended information about this item.</p>
</div>
#details-popup {
  position: fixed;
  top: anchor(bottom);
  left: anchor(left);
  margin: 0;
  margin-top: 0.5rem;
  padding: 1rem;
  border: 1px solid #ccc;
  border-radius: 0.5rem;
  max-width: 350px;
  position-try-fallbacks: flip-block;
}

Click the button and the popover appears below it, attached. Click elsewhere or press Escape and it closes. Light-dismiss, keyboard handling, and placement—all handled by the platform. The DFM2HTML JavaScript reference covers additional interaction patterns, but for many UI components, the platform APIs now handle what JavaScript libraries used to.

Browser Support in 2026

CSS anchor positioning had a staggered rollout across browsers:

  • Chrome / Edge: Full support since version 125 (May 2024)
  • Firefox: Support landed in version 131 (October 2024) with position-try-fallbacks following in version 133
  • Safari: Support arrived in Safari 18.2 (early 2025) with progressive improvements through Safari 19

By mid-2026, anchor positioning is fully supported across all major browsers. The position-try-fallbacks property—essential for robust tooltip and menu positioning—works everywhere that matters.

If you need to support significantly older browsers (pre-2024), you will need a JavaScript fallback for positioning. The Floating UI library remains the best option for calculated positioning. But for any site targeting current browsers, pure CSS anchor positioning is production-ready.

When to Still Use JavaScript

Anchor positioning covers static attachment—element B is positioned relative to element A. It does not cover:

  • Dynamic anchor selection. If the anchor changes based on user interaction (hovering over different items in a list to show a shared tooltip), you need JavaScript to update anchor-name or position-anchor dynamically.
  • Complex animation sequences. CSS transitions work fine for opacity and transform, but choreographed entrance animations—a callout that slides in from the side, pauses, then points an arrow—require JavaScript or the Web Animations API.
  • Anchor elements inside scroll containers. When the anchor scrolls while the positioned element is fixed to the viewport, anchor positioning tracks the anchor’s position. But if you need the positioned element to stay visible when the anchor scrolls out of view (a “sticky” tooltip), you need JavaScript scroll tracking.

For the patterns that most static sites need—menus below navigation triggers, tooltips above icons, callouts beside form fields—CSS anchor positioning is sufficient. JavaScript becomes necessary only for dynamic or complex interaction choreography.

Building Accessible Anchor-Positioned UI

Anchor positioning is a layout mechanism. It does not convey meaning or relationships to assistive technology. A tooltip positioned next to a form field looks attached visually, but a screen reader user does not know the tooltip exists unless you connect them semantically.

Always pair anchor positioning with appropriate ARIA:

<label for="email">Email</label>
<input
  type="email"
  id="email"
  aria-describedby="email-hint"
  style="anchor-name: --email-input"
>
<div
  id="email-hint"
  role="tooltip"
  style="position-anchor: --email-input"
>
  We'll never share your email with anyone.
</div>

The aria-describedby attribute tells screen readers that the hint text describes the input. The anchor positioning controls where it appears visually. Both layers are necessary.

For menus and popovers, the same principle applies: use aria-expanded, aria-haspopup, and proper role attributes as described in the dropdown menu guide. Anchor positioning replaces the layout JavaScript, not the accessibility markup.

Common Pitfalls and Debugging

Anchor positioning is new enough that debugging is not always intuitive. A few problems come up repeatedly.

The positioned element does not appear. Check that the anchor element actually has anchor-name set and that the positioned element references the same name via position-anchor. If the anchor name is misspelled—easy to do with dashed-idents—the positioned element falls back to normal positioning and ends up at 0,0 in the viewport.

The positioned element appears but in the wrong place. Verify which edge you are referencing. top: anchor(bottom) places the positioned element’s top edge at the anchor’s bottom edge. bottom: anchor(top) places the positioned element’s bottom edge at the anchor’s top edge. The property name refers to the positioned element’s edge; the anchor() argument refers to the anchor’s edge. Mixing these up is the most common positioning mistake.

The positioned element is clipped. If you use position: absolute instead of position: fixed, the element is positioned within its containing block and can be clipped by overflow: hidden on any ancestor. Use position: fixed for top-layer rendering, especially for tooltips and menus that must escape overflow containers.

Multiple anchors with the same name. Only the last element in document order with a given anchor-name is used as the anchor. If you have a list of items that each need their own anchor, use unique names or generate them dynamically with inline styles.

Chrome DevTools has started adding anchor positioning visualisation. In the Elements panel, selecting an anchor-positioned element highlights the anchor it references and draws lines showing the positioning relationship. This makes debugging substantially easier than inspecting computed styles.

Performance Implications

Anchor positioning is a pure CSS layout mechanism. The browser resolves anchor positions during its normal layout pass. There is no additional rendering pipeline, no observer callbacks, and no script execution. For static sites, the performance impact is effectively zero.

Compare this to the JavaScript alternative. A library like Floating UI runs calculations on every scroll event, every resize, and every layout change to keep a positioned element attached to its anchor. On a page with multiple tooltips or menus, this means dozens of DOM reads and writes per frame during scrolling. Anchor positioning eliminates all of that computation. The browser handles it natively, as part of the layout it was already computing.

If your site passes Core Web Vitals today, adding anchor positioning will not change that. You are removing JavaScript and replacing it with CSS, which is strictly a performance improvement.

Practical Patterns for DFM2HTML Sites

DFM2HTML generates clean HTML with predictable class names and semantic structure, which makes adding anchor positioning straightforward. The features page details the output format. Here are the patterns that work well:

Add anchor-name to your nav triggers in the stylesheet (or inline styles if you prefer per-element anchors). Position the dropdown menus using position-anchor and anchor(). Combine with the Popover API for toggle behaviour. The trigger button gets the anchor name; the dropdown <ul> gets the position reference. Because the dropdown uses position: fixed, it sits above all other content without requiring z-index management on the navigation bar.

Form Field Hints

Style contextual help text with anchor positioning so it appears beside the relevant input on wide screens and below it on narrow screens. Use position-try-fallbacks to handle the layout shift—the browser automatically moves the hint below the input when there is not enough horizontal space for a side callout. This gives you responsive form hints without a single media query or JavaScript calculation.

Feature Callouts

For marketing pages with feature highlights, anchor-position callout boxes to specific UI elements in screenshots or diagrams. They stay attached as the layout reflows. This is particularly useful for product pages where you want to annotate a screenshot with labelled arrows or boxed descriptions pointing to specific features.

Footnote-Style Annotations

Anchor-position footnote content to the reference mark in the text. On wide layouts, the footnote appears in the margin. On narrow layouts, position-try-fallbacks moves it below the paragraph. This pattern creates a reading experience similar to academic papers or annotated documentation—margin notes that relate directly to their source text.

These patterns leverage the container query techniques for component-level responsiveness combined with anchor positioning for precise attachment. Together, they cover the layout problems that previously required JavaScript libraries or convoluted CSS hacks.

A Practical Checklist

When implementing anchor-positioned UI on a static site:

  1. Assign anchor-name to trigger elements. Use descriptive dashed-ident names: --nav-services, --email-help, --feature-callout.
  2. Set position: fixed on the positioned element. This places it in the top layer, avoiding z-index and overflow issues.
  3. Use position-anchor and anchor() for placement. Reference the anchor edges you need: top, bottom, left, right, center.
  4. Add position-try-fallbacks for overflow handling. At minimum, use flip-block for tooltips and flip-inline for side-positioned elements.
  5. Connect elements semantically with ARIA. aria-describedby for hints, aria-expanded for menus, role="tooltip" for tooltips.
  6. Test with keyboard navigation. Anchor-positioned elements must be reachable and dismissible via keyboard.
  7. Test at various viewport sizes. Verify that fallback positions trigger correctly when space is constrained.

The tutorials index covers the related patterns—popover-based menus, accessible tooltips, and container query layouts—that combine with anchor positioning to build complete, JavaScript-light UI components.


← Back to Tutorials Download DFM2HTML