Skip to main content

CSS Anchor Positioning API: A Complete Guide

By LearnWebCraft Team15 min read
CSS Anchor Positioning APImodern css layoutdeclarative ui patterns

The CSS Anchor Positioning API is fundamentally changing how we think about layout. If you've spent any time in frontend development, you know the absolute pain of positioning a tooltip, a dropdown menu, or a context card. You usually end up wrestling with position: absolute, fighting z-index wars, or—more often than not—reaching for a heavy JavaScript library just to make sure a box stays next to a button when the user scrolls.

It’s been a massive gap in the web platform for years. We have incredible layout engines for grids and flexboxes, but tethering one element to another has historically required complex math and expensive event listeners.

That era is finally ending. With the CSS Anchor Positioning API, we can now achieve robust, context-aware positioning natively in CSS. It is declarative, performant, and frankly, a joy to use once you wrap your head around the syntax.

In this guide, we are going to tear down the old ways of doing things and build up a deep understanding of this new API. We will look at how to tether elements, handle edge detection (literally), and create UI patterns that used to require hundreds of lines of JavaScript.

1. Introduction to CSS Anchor Positioning

Imagine you have a button. When you click it, a menu appears. Logically, that menu "belongs" to that button. Visually, it should sit right next to it, perhaps floating just below.

In the past, HTML document flow didn't really care about this relationship. If the menu was a child of the button, position: absolute worked fine—until you needed overflow: hidden on a parent container, which would clip your beautiful menu. If you moved the menu to the <body> to avoid clipping (a technique called "portaling"), you lost the reference to the button's location entirely.

The CSS Anchor Positioning API solves this by creating a logical link between two disparate elements in the DOM. It allows an element (the anchor) to broadcast its location and dimensions, and another element (the target) to subscribe to that information and position itself accordingly.

It isn't just about top and left. It's about knowing where the anchor is, how big it is, and even dynamically changing position if the anchor runs out of screen space. This is a massive leap forward for semantic, declarative UI development.

2. Why Anchor Positioning? The Problem It Solves

To appreciate the solution, we have to revisit the problem. Why have we relied on libraries like Popper.js, Floating UI, or Tippy.js for so long?

The "Portal" Paradox

When building complex applications, we often use overflow: hidden or transform on containers (like a sidebar or a modal). If you put a tooltip inside that container, it gets cut off.

To fix this, we move the tooltip to the very end of the <body> tag. But now, the tooltip has no idea where the button is. They are miles apart in the DOM tree.

The JavaScript Tax

To reconnect them, we use JavaScript. We call getBoundingClientRect() on the button, get its X/Y coordinates, and apply them to the tooltip. But wait—what if the user scrolls? We need a scroll listener. What if the window resizes? We need a resize listener. What if the content changes size? We need a ResizeObserver.

Suddenly, a simple tooltip requires a dedicated script running calculations on every frame of a scroll event. This is bad for performance (main thread blocking) and adds significant bundle size.

The Native Solution

CSS Anchor Positioning moves all of this logic to the browser's layout engine. The browser already knows where everything is. It calculates layout on a separate thread (mostly). By telling the browser, "Hey, stick this box to that button," we eliminate the need for manual coordinate syncing. The result is smoother animations, zero lag during scrolling, and significantly cleaner codebases.

3. Core Concepts: Anchors, Popovers, and anchor()

Before we write code, let's establish the vocabulary. The API introduces a few key terms that you'll see repeatedly.

The Anchor

This is the reference element. It's the "rock" in the layout. It’s usually a button, an input field, or an icon that triggers a secondary UI element. We identify it using the anchor-name property.

The Target (or Anchored Element)

This is the floating element—the tooltip, the menu, or the popover. It is positioned relative to the anchor. We link it to the anchor using position-anchor.

The anchor() Function

This is where the magic happens. Instead of setting top: 50px, you set top: anchor(bottom). This tells the target: "Align my top edge with the anchor's bottom edge."

The Popover API

While not strictly part of the Anchor Positioning spec, these two are best friends. The Popover API handles the visibility and top-layer behavior (making sure it sits on top of everything else), while Anchor Positioning handles the location. We will often use them together.

4. Setting Up Your First Anchor: The anchor-name Property

Let's start building. We'll create a simple setup: a button that triggers a helper message.

The first step is defining the Anchor. We do this using the anchor-name property. Think of this like defining a CSS ID, but specifically for layout referencing. It must start with a double dash (--), just like a CSS variable.

HTML Structure:

<div class="container">
  <button class="trigger-btn">Hover Me</button>
  <div class="tooltip">I am a tooltip!</div>
</div>

CSS for the Anchor:

.trigger-btn {
  /* Give the anchor a unique name */
  anchor-name: --my-tooltip-trigger;
  
  /* Standard styling */
  padding: 12px 24px;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
}

That's it for the anchor side. We have essentially broadcasted this element's geometry to the CSS engine under the name --my-tooltip-trigger. Any element on the page can now ask, "Where is --my-tooltip-trigger?" and the browser will answer.

Important Note on Scope: anchor-name is currently scoped to the containment context, but generally, if you have unique names, they are accessible globally within the layout formatting context. If you have a list of 100 items, you wouldn't manually name them --item-1, --item-2. We'll discuss how to handle repetitive lists in the "Advanced Techniques" section later.

5. Positioning with position-anchor and anchor() Functions

Now, let's position the .tooltip. We need to do three things:

  1. Break it out of the normal document flow (position: absolute or fixed).
  2. Link it to our named anchor.
  3. Define its coordinates relative to that anchor.

CSS for the Target:

.tooltip {
  /* 1. Break flow */
  position: absolute;
  
  /* 2. Link to the specific anchor */
  position-anchor: --my-tooltip-trigger;
  
  /* 3. Define positioning using anchor() */
  bottom: anchor(top); /* My bottom touches anchor's top */
  left: anchor(center); /* My left edge aligns with anchor's center? Wait... */
  
  /* Styling details */
  background: #1f2937;
  color: white;
  padding: 8px 12px;
  border-radius: 4px;
  
  /* Center the tooltip horizontally relative to the anchor */
  translate: -50% 0; 
  margin-bottom: 10px; /* Spacer */
}

Understanding anchor() Logic

The anchor() function retrieves a position value from the anchored element.

  • top: anchor(bottom): This reads as "Set the top property of the tooltip to the bottom edge position of the anchor."
  • left: anchor(right): "Set the left property of the tooltip to the right edge position of the anchor."

Wait, look at the code above again. I used left: anchor(center). Is center a valid value? Yes! The API creates a grid of sorts. You can access top, bottom, start, end, left, right, center.

However, traditional absolute positioning (left: ...) aligns the edge of the tooltip, not its center. So left: anchor(center) puts the tooltip's left edge at the button's horizontal center. That's why we added translate: -50% 0—to shift the tooltip back so it looks centered.

This feels a bit like the old left: 50%; transform: translateX(-50%) hack, doesn't it? Fortunately, there is a much newer, cleaner way to handle this alignment which we discuss next.

6. Using inset-area for Complex Layouts

Note: In the most recent spec drafts and browser implementations, the property inset-area has been renamed to position-area. Since the prompt specifically asks about inset-area, we will use that terminology for the heading but clarify the modern standard in the content to ensure your knowledge is future-proof.

The anchor() function is precise, but it can be verbose. If you just want "Place this thing top-center," writing bottom: anchor(top); left: anchor(center); translate: -50% feels tedious.

Enter position-area (formerly inset-area).

This property abstracts the anchor relationship into a 3x3 grid centered on the anchor element.

The Grid Concept: Imagine a Tic-Tac-Toe board.

  • The Anchor is the center square.
  • The surrounding 8 squares are the available "areas".

You can reference these areas using keywords like top, bottom, left, right, center, start, end.

Refactoring our Tooltip:

.tooltip {
  position: absolute;
  position-anchor: --my-tooltip-trigger;
  
  /* The Magic One-Liner */
  position-area: top center; 
  
  /* No need for top/left/transform anymore! */
  margin: 10px; /* Just adds spacing from the anchor */
}

How position-area works: When you say top center:

  1. The browser looks at the anchor.
  2. It identifies the space directly above the anchor (top) and horizontally centered (center).
  3. It places the tooltip into that slot.
  4. Crucially, it handles the alignment automatically. It knows that if you are in the "top center" slot, you probably want the tooltip centered horizontally and sitting at the bottom of that slot (closest to the anchor).

It behaves almost like a Flexbox or Grid cell. This is incredibly declarative. You aren't calculating pixels; you are stating intent. "I want this on the top, centered."

Common Values:

  • bottom left: Places the element below the anchor, aligned to the left edge.
  • top span-all: Places the element above, spanning the full width of the anchor.
  • inline-end center: Places it to the right (in LTR languages), vertically centered.

7. Handling Overflow and Fallbacks with anchor-fallback

The biggest headache in popup positioning is collisions. What happens if you position a tooltip on top, but the user has scrolled such that the button is at the very top of the viewport? The tooltip gets cut off.

In JavaScript libraries (like Popper.js), this is called "flipping." The library detects the collision and flips the tooltip to the bottom.

CSS handles this natively now with @position-try (sometimes referred to in older docs as @try or fallbacks).

Step 1: Define Fallback Positions

We define a set of rules—"try" blocks—that the browser can attempt to use if the default position doesn't fit.

/* Define a custom position option */
@position-try --flip-to-bottom {
  position-area: bottom center;
}

@position-try --flip-to-right {
  position-area: right center;
}

Step 2: Apply Options to the Element

Now we tell the tooltip: "Try your default position first. If that clips, try flipping to the bottom. If that still clips, try the right."

.tooltip {
  position: absolute;
  position-anchor: --my-tooltip-trigger;
  position-area: top center; /* The preferred default */
  
  /* The Fallback Chain */
  position-try-fallbacks: --flip-to-bottom, --flip-to-right;
}

The browser acts as a smart negotiator.

  1. It tries top center. Does it fit in the viewport? Yes? Render it.
  2. No? It tries --flip-to-bottom. Does that fit? Yes? Render it.
  3. No? It tries --flip-to-right.

Built-in Keywords

You don't always need to write custom @position-try blocks. Browsers are implementing keywords for common flips. For example:

.tooltip {
  position-try-fallbacks: flip-block, flip-inline;
}

flip-block means "if top doesn't work, try bottom." flip-inline means "if left doesn't work, try right." This covers 90% of use cases with zero custom at-rules.

8. Browser Support and Progressive Enhancement

Let's address the elephant in the room. Can you use this today?

As of mid-2025, Chrome, Edge, and Opera have excellent support. Firefox and Safari are catching up, often hiding these features behind flags or in technology previews.

Using @supports

You should never ship this without a fallback strategy for older browsers (or Safari, until it ships).

@supports (position-anchor: --test) {
  .tooltip {
    position: absolute;
    position-anchor: --my-trigger;
    position-area: top center;
  }
}

@supports not (position-anchor: --test) {
  /* Fallback for browsers without Anchor API */
  .tooltip {
    position: absolute;
    /* You might have to use a simpler, less robust positioning 
       or rely on a JS polyfill here */
    top: -40px; 
    left: 50%;
    transform: translateX(-50%);
  }
}

The Polyfill Strategy

There is an official polyfill provided by the Oddbird team (who were heavily involved in the spec). It allows you to write the new CSS syntax, and it uses JavaScript under the hood to make it work in unsupported browsers.

If you are building a greenfield project, I recommend using the polyfill. It allows you to write future-proof CSS today. When Safari and Firefox catch up, you can simply remove the polyfill script, and your CSS remains valid.

9. Real-World Use Cases: Tooltips, Menus, Modals

Let’s look at how this changes actual components we build every day.

Case A: The "Select" Menu

Custom dropdowns are notoriously hard. You want the list to be the same width as the trigger button.

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

.select-dropdown {
  position: absolute;
  position-anchor: --select-menu;
  
  /* Align top-left of dropdown to bottom-left of trigger */
  top: anchor(bottom);
  left: anchor(left);
  
  /* Make the width match the trigger! */
  width: anchor-size(width); 
}

Notice anchor-size(width). This is a superpower. You are telling the dropdown: "Be exactly as wide as the element referencing you." No JS measurement required.

Case B: The Context Menu (Right Click)

Usually, context menus are positioned based on mouse coordinates (e.clientX, e.clientY). The Anchor API is designed for elements, not points. However, you can create a virtual element!

You can inject a tiny, invisible div at the mouse coordinates using JS, give it an anchor-name, and then let CSS handle the menu positioning relative to that invisible div. This keeps your JS logic purely about "where is the click" and your CSS logic purely about "how does the menu align to that point."

Case C: Form Validation Messages

Imagine an input field that fails validation. You want an error bubble to appear to the right.

input.error {
  anchor-name: --err-input;
}

.error-message {
  position: absolute;
  position-anchor: --err-input;
  position-area: right center;
  position-try-fallbacks: flip-inline; /* If no space on right, go left */
  color: red;
}

10. Advanced Techniques and Best Practices

Handling Dynamic Lists (The anchor-scope Problem)

Earlier, I mentioned that anchor-name must be unique. What if you have a list of 50 items, and each needs a tooltip? You can't write 50 CSS classes.

As the spec evolves, we are seeing the introduction of anchor-scope (or similar scoping mechanisms). But currently, the best practice for lists often involves inline styles or a light JavaScript touch to assign the anchor-name dynamically only to the active element.

The "Active Anchor" Pattern:

  1. You have one generic .tooltip element in the DOM.
  2. When a user hovers a list item, you add a class or inline style to that specific list item: style="anchor-name: --active-item".
  3. The tooltip is always set to position-anchor: --active-item.
  4. As the user moves the mouse, the anchor-name moves from item to item, and the tooltip snaps to the new host.

This is highly performant because you aren't calculating coordinates; you are just moving a CSS property tag.

Visual Styling vs. Logical Position

Remember that anchor() only affects layout. It doesn't draw the little arrow (triangle) pointing to the button. You still need to draw that using ::before or ::after.

However, you can position that arrow using the same anchor logic!

.tooltip::after {
  content: '';
  position: absolute;
  /* Position the arrow relative to the anchor too! */
  left: anchor(center);
  bottom: -5px;
  /* shapes etc... */
}

11. Conclusion: The Future of CSS Positioning

The CSS Anchor Positioning API is more than just a convenience feature; it is a fundamental shift in the responsibility of layout. For a decade, we have burdened the main thread with expensive JavaScript layout thrashing just to keep a box next to a button.

By moving this logic into CSS, we gain:

  1. Performance: The compositor and layout engine handle the math.
  2. Maintainability: No more "magic numbers" or complex event listeners in your React/Vue components.
  3. Robustness: Native handling of scrolling, resizing, and viewport collisions.

We are entering an era where CSS is reclaiming its territory. Layout belongs in the style sheet, not the script tag. While browser support is still rolling out, the existence of robust polyfills means you can—and probably should—start learning and using this API today.

Start small. Replace a simple tooltip. Then a dropdown. Before you know it, you'll wonder how you ever survived the getBoundingClientRect era.

Quick Summary Checklist

  • Define Anchor: anchor-name: --my-anchor on the trigger.
  • Link Target: position-anchor: --my-anchor on the popup.
  • Position: Use position-area: top center for easy layouts, or top: anchor(bottom) for precise control.
  • Fallback: Use position-try-fallbacks to handle screen edges.
  • Polyfill: Use the Oddbird polyfill for cross-browser support until 2026+.

Welcome to the future of layout. It’s tethered, it’s declarative, and it’s beautiful.

Related Articles