Skip to content

Web Development · CSS

CSS Anchor Positioning: Tooltips and Popovers Without JavaScript

CSS Anchor Positioning lets you position one element relative to another using only CSS. No Popper.js, no JavaScript positioning logic, no layout recalculation on scroll. Here's how it works.

Anurag Verma

Anurag Verma

7 min read

CSS Anchor Positioning: Tooltips and Popovers Without JavaScript

Sponsored

Share

Positioning a tooltip or dropdown menu relative to a button has always been an awkward task in CSS. The standard approach for the last decade: JavaScript reads the button’s bounding rect, calculates where the tooltip should appear, and sets top and left in inline styles. Libraries like Popper.js and Floating UI exist specifically to handle the edge cases: scroll offsets, viewport overflow, resizing, nested scroll containers.

CSS Anchor Positioning changes this. It’s a native CSS mechanism for positioning one element relative to another, with built-in fallback logic for when the positioned element would overflow the viewport. It shipped in Chrome 125 in May 2024, Firefox 131 in September 2024, and Safari 18.2 in December 2024. As of 2026, it’s available across all major browsers with no prefix required.

The Core Idea

Anchor Positioning works with two declarations:

  1. Name an element as an anchor with anchor-name
  2. Position another element relative to that anchor with position-anchor and the anchor() function

The element being positioned must be position: absolute or position: fixed. The positioned element doesn’t need to be a child of the anchor; it can live anywhere in the DOM.

/* The anchor element */
.tooltip-trigger {
  anchor-name: --my-button;
}

/* The positioned element */
.tooltip {
  position: absolute;
  position-anchor: --my-button;

  /* Position the tooltip's bottom edge 8px above the anchor's top edge */
  bottom: calc(anchor(top) - 8px);

  /* Center the tooltip horizontally over the anchor */
  left: anchor(center);
  translate: -50% 0;
}

The anchor() function references edges and center points of the named anchor: top, right, bottom, left, center, start, and end (logical properties for inline/block axes).

A Working Tooltip

Here’s a complete tooltip that appears above its trigger:

<button class="btn" popovertarget="my-tooltip">
  Hover me
</button>

<div id="my-tooltip" class="tooltip" popover="manual">
  This is the tooltip content
</div>
.btn {
  anchor-name: --trigger;
}

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

  /* Position above the trigger with 8px gap */
  bottom: calc(anchor(top) - 8px);
  left: anchor(center);
  translate: -50% 0;

  /* Visual styling */
  background: #1e293b;
  color: white;
  padding: 6px 12px;
  border-radius: 6px;
  font-size: 14px;
  white-space: nowrap;

  /* Smooth show/hide transition */
  transition: opacity 0.15s, display 0.15s allow-discrete;
  opacity: 0;
}

.tooltip:popover-open {
  opacity: 1;
}

@starting-style {
  .tooltip:popover-open {
    opacity: 0;
  }
}

Note position: fixed instead of position: absolute. Fixed positioning places the element relative to the viewport, which means it works correctly regardless of scroll position or overflow on parent elements (the same reason you’d use position: fixed for a JavaScript tooltip). Anchor Positioning updates the fixed element’s position automatically as the page scrolls or the anchor moves.

Handling Viewport Overflow with @position-try

The real power over JavaScript solutions comes from @position-try. This lets you define fallback positioning rules that activate when the primary position would cause overflow:

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

  /* Primary: above the trigger */
  bottom: calc(anchor(top) - 8px);
  left: anchor(center);
  translate: -50% 0;

  /* Try these in order if the primary overflows */
  position-try-fallbacks: --below, --right, --left;
}

/* Fallback: below the trigger */
@position-try --below {
  top: calc(anchor(bottom) + 8px);
  bottom: unset;
  left: anchor(center);
  translate: -50% 0;
}

/* Fallback: to the right */
@position-try --right {
  top: anchor(center);
  left: calc(anchor(right) + 8px);
  translate: 0 -50%;
  bottom: unset;
}

/* Fallback: to the left */
@position-try --left {
  top: anchor(center);
  right: calc(100% - anchor(left) + 8px);
  left: unset;
  translate: 0 -50%;
  bottom: unset;
}

The browser evaluates each @position-try rule in order and uses the first one that doesn’t overflow the viewport. This is behavior that required dozens of lines of JavaScript to replicate with Popper.js. Here it’s declarative CSS.

A more complete example: a dropdown menu that positions below its trigger, with overflow fallback:

<button class="menu-trigger" popovertarget="main-menu">
  Options ▾
</button>

<ul id="main-menu" class="menu" popover>
  <li><a href="#">Edit</a></li>
  <li><a href="#">Duplicate</a></li>
  <li><a href="#">Delete</a></li>
</ul>
.menu-trigger {
  anchor-name: --menu-anchor;
}

.menu {
  position: fixed;
  position-anchor: --menu-anchor;

  /* Align left edge to trigger's left edge, appear below */
  top: calc(anchor(bottom) + 4px);
  left: anchor(left);

  /* Reset popover default styles */
  margin: 0;
  padding: 4px 0;
  list-style: none;
  background: white;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
  min-width: 160px;

  /* Overflow fallback: try right-aligned if left overflows */
  position-try-fallbacks: --align-right;
}

@position-try --align-right {
  left: unset;
  right: calc(100% - anchor(right));
}

.menu a {
  display: block;
  padding: 8px 16px;
  color: #1e293b;
  text-decoration: none;
}

.menu a:hover {
  background: #f8fafc;
}

The popover attribute handles show/hide behavior (tied to the popovertarget button) and focus management. The Popover API and CSS Anchor Positioning are designed to work together.

inset-area: The Shorthand

For common cases, inset-area provides a shorthand that describes position relative to the anchor using a grid-like syntax:

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

  /* Same as: above the anchor, horizontally centered */
  inset-area: top center;
}

Other values:

inset-area: bottom center;   /* below */
inset-area: top left;        /* above and aligned to left edge */
inset-area: right center;    /* to the right, vertically centered */
inset-area: start end;       /* logical: end of block axis, end of inline axis */

inset-area is more limited than the anchor() function: you can’t express “8px above” with it, only “above.” For tooltips with precise spacing, use anchor() with calc(). For rough positioning or simple cases, inset-area reads clearly.

Multiple Anchors

An element can reference multiple anchors. The CSS custom property names are used to distinguish them:

.connector-line {
  position: fixed;

  /* Stretch from one anchor to another */
  left: anchor(--start right);
  top: anchor(--start center);
  right: calc(100% - anchor(--end left));
  bottom: calc(100% - anchor(--end center));
}

This enables connector lines, range selection highlighting, and comparison callouts, all without JavaScript measuring element positions.

What You Can Drop

If you’re using Floating UI or Popper.js solely for tooltip and popover positioning, you can replace them with CSS Anchor Positioning for modern browser targets. The core feature set (position relative to another element, flip when near viewport edge) is covered natively.

What these libraries still offer that CSS doesn’t (yet):

  • Middleware for arbitrary transformations during position calculation
  • Virtual reference elements (anchoring to a cursor position or arbitrary coordinates)
  • Detailed position data surfaced to JavaScript for custom rendering logic

For 80% of tooltip and dropdown use cases, those extra features aren’t needed. The JavaScript overhead and bundle weight of Floating UI can be replaced with a few lines of CSS.

Browser support in 2026 is solid enough that you don’t need a polyfill for most projects. Check caniuse.com/css-anchor-positioning for the current status — it’s been Baseline Available since early 2025.

Browser Support Note

CSS Anchor Positioning is fully supported in Chrome 125+, Edge 125+, Firefox 131+, and Safari 18.2+. If your target audience includes users on older browsers, test your fallback experience. Elements without anchor positioning will use their normal static or positioned flow, which may or may not be acceptable depending on the UI.

For progressive enhancement, check for support before relying on it:

@supports (anchor-name: --test) {
  .tooltip {
    /* anchor-based positioning */
    position: fixed;
    position-anchor: --trigger;
    bottom: calc(anchor(top) - 8px);
    left: anchor(center);
    translate: -50% 0;
  }
}

Without the @supports block, the tooltip can fall back to a simpler always-visible or JavaScript-positioned implementation.

The API is still evolving. Some edge cases around containing block behavior, anchor scope (preventing cross-shadow-root references), and scroll-anchoring interaction are being worked through in the spec. But the core functionality (position A relative to B with overflow fallback) is stable and ready to use.

Sponsored

Enjoyed it? Pass it on.

Share this article.

Sponsored

The dispatch

Working notes from
the studio.

A short letter twice a month — what we shipped, what broke, and the AI tools earning their keep.

No spam, ever. Unsubscribe anytime.

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored