Precise Positioning with CSS Anchor Functions: anchor(), anchor-center, and anchor-size()

Take your anchor positioning beyond the grid with functions that give you pixel-level control over placement and sizing.

In the previous post, we explored how position-area places elements using a 3×3 grid around the anchor. That covers the common cases beautifully, tooltips above buttons, dropdowns below nav items, popovers beside icons.

But sometimes the grid isn’t enough. Maybe you need a precise gap between elements. Maybe you want a tooltip to track along a slider at a specific percentage. Maybe your dropdown should match its trigger button’s width exactly.

That’s where anchor functions come in.

Baseline Status

The anchor() Function

While position-area thinks in grid regions, anchor() lets you reference specific edges of your anchor and use them directly in inset properties.

Here’s what the spec says:

An anchor-positioned element can use the anchor() function as a value in its inset properties to refer to the position of one or more anchor elements. The anchor() function resolves to a <length>.

In plain terms: anchor() returns a measurement — the distance from the edge of your containing block to a specific edge of the anchor. You can use this value in inset properties like inset-block-start, inset-inline-end, and their physical equivalents.

Basic Syntax

.tooltip {
  position: absolute;
  position-anchor: --button;

  /* Position my top edge at the anchor's bottom edge */
  inset-block-start: anchor(end);
}

The function takes an anchor side as its argument. Here are your options:

TypeKeywords
Physicaltop, right, bottom, left
Logicalstart, end, self-start, self-end
Contextualinside, outside
Percentage0% to 100%, or center (equivalent to 50%)

A subtle but important distinction: start and end resolve based on the anchor element’s writing mode, while self-start and self-end resolve based on the positioned element’s writing mode. In most cases these are identical, but if your tooltip has a different writing mode than its anchor, they’d point to different edges.

How It Actually Works

Here’s the key insight: anchor() returns the distance from the edge of the containing block to the specified edge of the anchor.

When you write:

.tooltip {
  position: fixed;
  position-anchor: --button;
  inset-block-end: anchor(start);
}

You’re saying: “Position my element so its block-end (bottom) edge aligns with the anchor’s block-start (top) edge.” The result? Your tooltip sits directly above the button.

┌─────────────────────────────────────────┐
│                                         │
│              ┌──────────┐               │
│              │ TOOLTIP  │               │
│              └──────────┘ ← block-end   │
│              ┌──────────┐ ← anchor      │
│              │  BUTTON  │   block-start │
│              └──────────┘               │
│                                         │
└─────────────────────────────────────────┘

Adding Gaps with calc()

Because anchor() returns a length, you can use it in calculations:

.tooltip {
  position: fixed;
  position-anchor: --button;
  inset-block-end: calc(anchor(start) + 0.5rem);
}
┌─────────────────────────────────────────┐
│              ┌──────────┐               │
│              │ TOOLTIP  │               │
│              └──────────┘               │
│                 0.5rem                  │
│              ┌──────────┐               │
│              │  BUTTON  │               │
│              └──────────┘               │
└─────────────────────────────────────────┘

This is where anchor() shines over position-area. With position-area, gaps come from margins, a fixed offset that doesn’t “know about” the anchor. With anchor(), the anchor’s edge position is the input to your calculation.

The inside and outside Keywords

These are contextual, they resolve based on which inset property you’re using:

KeywordIn inset-block-startIn inset-block-end
insidestartend
outsideendstart

inside means “the same side”, useful for overlapping the anchor.

outside means “the opposite side”, useful for adjacent positioning.

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

  /* "outside" in inset-block-end means anchor's start edge */
  inset-block-end: anchor(outside);
}

This is equivalent to inset-block-end: anchor(start), but reads more semantically: “position my block-end (bottom) edge outside the anchor.”

Axis Restrictions

Here’s an important constraint: physical keywords can only be used in matching axes.

/* ✅ Valid — horizontal keyword in horizontal property */
left: anchor(right);

/* ✅ Valid — vertical keyword in vertical property */
top: anchor(bottom);

/* ❌ Invalid — vertical keyword in horizontal property */
left: anchor(top);

The logical keywords (start, end, self-start, self-end) resolve based on the property’s axis, so they work in any inset property:

/* ✅ Both valid — "end" resolves appropriately for each axis */
inset-inline-start: anchor(end); /* end = inline-end */
inset-block-start: anchor(end); /* end = block-end */

This is another reason to prefer logical keywords, they adapt to writing modes and sidestep axis-matching concerns entirely.

Percentage Positioning

Percentages let you position at any point along the anchor’s edge:

.slider-tooltip {
  position: fixed;
  position-anchor: --track;
  inset-inline-start: anchor(25%); /* 25% along the anchor's inline size */
  inset-block-end: anchor(start);
}

This is particularly useful for elements that need to track along an edge, like a tooltip showing a slider’s current value.

Referencing Multiple Anchors

Unlike position-area, which only references your default anchor, anchor() can reference different anchors for different edges:

.stretched-element {
  position: fixed;
  inset-block-start: anchor(--header end);
  inset-block-end: anchor(--footer start);
}

This stretches an element between a header and footer, something the 3×3 grid model simply can’t express.

Critical Gotcha: Popover UA Styles

If you’re using anchor() with the Popover API, you’ll hit a wall that isn’t obvious from the spec.

The browser’s default popover styles include:

[popover] {
  inset: 0;
  margin: auto;
}

These centre the popover in the viewport. When you use anchor() on one property (say inset-block-start: anchor(end)), that value does take effect. But the other inset properties remain 0 from the UA stylesheet. Combined with margin: auto, the element is still constrained on the axes you didn’t explicitly set, leading to unexpected positioning.

The fix:

[popover] {
  inset: auto; /* Clear all UA inset values */
  margin: unset; /* Clear the UA margin: auto */

  /* Now your anchor positioning works as expected */
  inset-block-start: anchor(end);
  inset-inline-start: anchor(start);
}

Important: Unlike position-area (which has more sensible UA defaults when you only specify one axis), anchor() only affects the specific property you use it in. You need to explicitly position both axes, or clear the defaults so the unset axis behaves predictably.

The anchor-center Value

Here’s a common scenario: you want to position an element to the inline-end of its anchor, vertically centred on it.

Your first instinct might be:

.tooltip {
  inset-inline-start: calc(anchor(end) + 0.5rem);
  inset-block-start: anchor(center);
}

But this doesn’t centre your tooltip, it positions the tooltip’s block-start (top) edge at the anchor’s vertical centre. The tooltip sits below centre.

anchor-center solves this. It’s a special value for align-self and justify-self that centres your element on its anchor along that axis.

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

  inset-inline-start: calc(
    anchor(end) + 0.5rem
  ); /* To the inline-end, with gap */
  align-self: anchor-center; /* Vertically centred on anchor */
}
┌─────────────────────────────────────────┐
│                         ┌─────────────┐ │
│              ┌────┐     │             │ │
│              │ICON│     │   TOOLTIP   │ │
│              └────┘     │             │ │
│                         └─────────────┘ │
└─────────────────────────────────────────┘

Quick Reference

PropertyAxisEffect
align-self: anchor-centerBlock axisCentres vertically (in horizontal writing modes)
justify-self: anchor-centerInline axisCentres horizontally (in horizontal writing modes)

One Constraint

anchor-center only works when you haven’t constrained that axis with an inset property. If you set inset-block-start or inset-block-end, you’ve already positioned the element on the block axis, there’s no room for align-self to centre it.

The anchor-size() Function

This function lets you size your positioned element based on your anchor’s dimensions. It returns a <length> for use in sizing properties.

Syntax

anchor-size(<dimension>)

Where <dimension> can be:

KeywordReturns
widthAnchor’s width
heightAnchor’s height
blockAnchor’s block-axis size
inlineAnchor’s inline-axis size
self-blockAnchor’s size in the positioned element’s block axis
self-inlineAnchor’s size in the positioned element’s inline axis

Basic Example

A dropdown that matches its trigger button’s inline size:

.dropdown {
  position: fixed;
  position-anchor: --trigger;
  position-area: block-end span-inline-start;

  inline-size: anchor-size(inline);
}

With Calculations

A tooltip that’s always at least as wide as its anchor:

.tooltip {
  min-inline-size: anchor-size(inline);
}

A popover that’s exactly twice the anchor’s inline size:

[popover] {
  inline-size: calc(anchor-size(inline) * 2);
}

Why self-block and self-inline Exist

The block and inline keywords resolve using the containing block’s writing mode. The self-block and self-inline keywords resolve using the positioned element’s own writing mode.

In most cases these are identical. But if you have a vertical-writing-mode element anchored inside a horizontal-writing-mode container, they’d differ.

No Axis Restrictions

Unlike anchor(), anchor-size() doesn’t care about axis matching:

/* Valid! Makes element's inline size match anchor's block size */
inline-size: anchor-size(block);

This enables patterns like a square element sized to the anchor’s larger dimension:

.square-indicator {
  inline-size: max(anchor-size(inline), anchor-size(block));
  aspect-ratio: 1;
}

Implicit Dimension

If you omit the dimension, it defaults to match the property’s axis:

/* These are equivalent */
inline-size: anchor-size();
inline-size: anchor-size(inline);

However, the explicit version is clearer, future-you won’t need to look up the default behaviour.

When to Use Which

After working through both approaches, here’s how to choose:

Use position-area when…Use anchor() when…
The 3×3 grid model covers your placement needsThe grid is too restrictive
You’re referencing one anchor at a timeYou need different anchors for different edges
Gaps via margin work fineGaps need to be calculated relative to anchor edges
Edge and corner alignment is sufficientYou need percentage-based positioning along an edge

These aren’t mutually exclusive. You might use position-area for basic placement and add anchor-size() for sizing:

.dropdown {
  position: fixed;
  position-anchor: --button;
  position-area: block-end span-inline-start;
  inline-size: anchor-size(inline); /* Match button width */
}

Practice Exercises

Exercise 1

Build a dropdown that appears below a button, with a 0.25rem gap, and inline-start-aligned with the button’s inline-start edge.

Solution
[popover] {
  inset: auto;
  margin: unset;

  inset-block-start: calc(anchor(end) + 0.25rem);
  inset-inline-start: anchor(start);
}

The inset-block-start: calc(anchor(end) + 0.25rem) positions the dropdown’s block-start (top) edge 0.25rem below the anchor’s block-end (bottom) edge. The inset-inline-start: anchor(start) aligns the inline-start (left) edges.

Exercise 2

Build a tooltip that appears to the inline-end of an icon, vertically centred on it, with a 0.5rem gap.

Solution
.tooltip {
  position: fixed;
  position-anchor: --icon;

  inset: auto;
  margin: unset;

  inset-inline-start: calc(anchor(end) + 0.5rem);
  align-self: anchor-center;
}

The anchor(end) references the icon’s inline-end (right) edge. The calc() adds the gap. align-self: anchor-center handles vertical centring.

Exercise 3

Build a popover that appears below a button, centred horizontally on it, and matches the button’s width.

Solution
[popover] {
  inset: auto;
  margin: unset;

  inset-block-start: anchor(end);
  justify-self: anchor-center;
  inline-size: anchor-size(inline);
}

Or, using position-area for placement with anchor-size() for sizing:

[popover] {
  position-area: block-end;
  inline-size: anchor-size(inline);
}

The second approach is simpler when the grid model suffices for placement.

What’s Next

We’ve now covered placement (position-area, anchor()), alignment (anchor-center), and sizing (anchor-size()). But what happens when your preferred position doesn’t fit? What if the tooltip would overflow the viewport?

The next post tackles position fallbacks, the system for defining alternative positions when your first choice causes problems:

Further Reading

This is part 2 of a series on CSS Anchor Positioning.