CSS anchor positioning: position popovers without JavaScript
CSS finally has a native way to position one element relative to another. The feature is called CSS anchor positioning, and it solves one of those frontend problems we have been handling with JavaScript, extra wrappers, and positioning calculations for years.
Think about tooltips, dropdown menus, popovers, labels, and small settings panels. They all need to appear next to the element that opened them. Until now, this usually meant reading the trigger’s position with JavaScript, calculating where the floating element should go, watching for scrolling and resizing, and moving it again when there was not enough space.
It works, but it always felt like something CSS should be able to do. Now it can.
The problem
Absolute positioning is useful, but it is normally based on an element’s containing block. If you want to place a dropdown under a button, the usual solution is to put both elements inside a positioned wrapper.
<div class="menu">
<button>Options</button>
<div class="dropdown">...</div>
</div>
.menu {
position: relative;
}
.dropdown {
position: absolute;
top: 100%;
left: 0;
}
This works when the elements are close together in the HTML and the layout is simple. It gets harder when the floating element is rendered somewhere else, placed in the top layer, or needs to move when it reaches the edge of the viewport.
JavaScript libraries solve this by measuring both elements and calculating the final position. CSS anchor positioning gives the browser a native way to create that relationship instead.
The basic idea
Anchor positioning works with two elements: an anchor and an anchor-positioned element.
The anchor is usually the button, input, link, or other element that the floating content belongs to. The positioned element is the tooltip, dropdown, popover, or panel that should appear next to it.
First, give the anchor a name:
.menu-button {
anchor-name: --menu-button;
}
Anchor names must begin with two dashes, similar to custom properties.
Next, connect the floating element to that anchor:
.menu-panel {
position: absolute;
position-anchor: --menu-button;
}
The positioned element must use position: absolute or position: fixed. Once the relationship exists, CSS can position the panel relative to the button instead of only relative to its containing block.
Positioning with position-area
The easiest way to place an anchored element is with position-area.
.menu-panel {
position: absolute;
position-anchor: --menu-button;
position-area: bottom;
}
This places the panel below the anchor. Other useful values include:
position-area: top;
position-area: right;
position-area: left;
position-area: bottom left;
position-area: bottom right;
You can think of the area around the anchor as a three-by-three grid. The anchor occupies the center, and position-area chooses where the positioned element should go around it.
To add some space between the elements, use margin:
.menu-panel {
position: absolute;
position-anchor: --menu-button;
position-area: bottom;
margin-top: 0.5rem;
}
The browser now knows that the panel belongs below the button. There is no need to calculate the button’s coordinates yourself.
A practical popover example
Anchor positioning handles where an element appears. It does not automatically handle opening, closing, keyboard controls, or clicking outside the element.
For opening and closing behavior, anchor positioning pairs well with the native Popover API.
<button
class="menu-button"
popovertarget="options-menu"
>
Options
</button>
<div
class="menu-panel"
id="options-menu"
popover
>
<a href="#">Edit</a>
<a href="#">Duplicate</a>
<button type="button">Delete</button>
</div>
The popovertarget attribute connects the button to the popover. The browser handles opening, closing, Escape key dismissal, and top-layer placement.
CSS anchor positioning handles where the popover appears:
.menu-button {
anchor-name: --menu-button;
}
.menu-panel {
position: fixed;
position-anchor: --menu-button;
position-area: bottom;
inset: auto;
margin: 0.5rem 0 0;
min-width: 12rem;
max-width: calc(100vw - 2rem);
}
The inset: auto and margin reset are important for popovers because browsers apply default positioning styles that can conflict with anchor positioning.
This gives us a working menu without JavaScript positioning calculations. The browser handles the relationship between the button and the panel, even though the open popover is displayed in the top layer.
Matching the width of the anchor
Sometimes you want the floating element to have the same width as its trigger. A common example is a dropdown that should match the width of an input or button.
The anchor-size() function gives us access to the dimensions of the anchor.
.menu-panel {
width: anchor-size(width);
}
A more defensive version could look like this:
.menu-panel {
width: anchor-size(width);
min-width: 12rem;
max-width: calc(100vw - 2rem);
}
The panel starts with the same width as its anchor, but it cannot become narrower than 12rem or wider than the available viewport.
You can also use anchor-size() with height, minimum sizes, maximum sizes, margins, and calculations.
.menu-panel {
min-width: anchor-size(width);
max-width: 24rem;
}
This is useful for dropdowns, autocomplete results, date pickers, and other panels that should be at least as wide as the field that opened them.
What happens near the edge of the screen?
Positioning a panel below a button works until the button is close to the bottom of the viewport. If there is not enough space, the panel can overflow offscreen.
This is another problem that JavaScript positioning libraries usually solve. They check the available space and flip the element to another side.
CSS anchor positioning can do that too.
.menu-panel {
position: fixed;
position-anchor: --menu-button;
position-area: bottom;
position-try-fallbacks: flip-block;
}
The preferred position is below the button. If there is not enough vertical space, flip-block tells the browser to try the opposite side.
You can provide more than one fallback:
.menu-panel {
position-try-fallbacks:
flip-block,
flip-inline,
flip-block flip-inline;
}
The browser tries the options in order until it finds one that fits without overflowing.
This is probably one of the most useful parts of anchor positioning. The browser already knows the size of the viewport, the position of the anchor, and the size of the floating element. We no longer need to repeat that work in JavaScript.
More precise positioning with anchor()
position-area covers most common placements, but sometimes you need more control. The anchor() function lets you position an element using a specific edge or point of its anchor.
For example, this places a tooltip below the center of a button:
.help-button {
anchor-name: --help-button;
}
.tooltip {
position: fixed;
position-anchor: --help-button;
top: anchor(bottom);
left: anchor(center);
transform: translateX(-50%);
margin-top: 0.5rem;
}
top: anchor(bottom) places the tooltip’s top edge at the bottom edge of the anchor. left: anchor(center) starts it at the horizontal center of the anchor, and the transform moves the tooltip back by half of its own width.
You can also use anchor() inside calc():
.tooltip {
top: calc(anchor(bottom) + 0.5rem);
left: anchor(center);
}
I would start with position-area because it is easier to read. Use anchor() when the design needs more exact control.
Repeated components
There is one important detail when using anchor positioning in repeated components.
If several elements have the same anchor-name, a positioned element can resolve to the last matching anchor in the document. That can cause every dropdown or tooltip to attach itself to the same trigger.
The anchor-scope property limits an anchor name to a specific component subtree.
<div class="menu-component">
<button class="menu-button">Options</button>
<div class="menu-panel">...</div>
</div>
.menu-component {
anchor-scope: --menu-button;
}
.menu-button {
anchor-name: --menu-button;
}
.menu-panel {
position: absolute;
position-anchor: --menu-button;
position-area: bottom;
}
Now the panel can only connect to the matching anchor inside the same component.
This makes it possible to reuse the same anchor name instead of generating a different CSS name for every button on the page.
A practical default
For a simple anchored popover, this is a good starting point:
.popover-trigger {
anchor-name: --popover-trigger;
}
.popover-panel {
position: fixed;
position-anchor: --popover-trigger;
position-area: bottom;
position-try-fallbacks: flip-block, flip-inline;
inset: auto;
margin: 0.5rem 0 0;
min-width: 12rem;
max-width: calc(100vw - 2rem);
max-height: calc(100vh - 2rem);
overflow: auto;
}
If the panel should match the width of the trigger, add:
.popover-panel {
width: anchor-size(width);
}
The minimum and maximum sizes are not required for anchor positioning to work. They are there to keep the panel usable when the content is long or the available viewport is small.
Progressive enhancement
CSS anchor positioning is now available in current versions of all major browser engines, but older browsers still exist. I would treat it as progressive enhancement rather than making essential content depend on it.
You can check the full CSS anchor positioning documentation on MDN and current browser support on Can I Use.
For a popover, the fallback can simply be the browser’s default centered placement. Put the anchor-specific styles inside @supports:
.menu-panel {
min-width: 12rem;
max-width: calc(100vw - 2rem);
}
@supports (anchor-name: --test) and (position-area: bottom) {
.menu-button {
anchor-name: --menu-button;
}
.menu-panel {
position: fixed;
position-anchor: --menu-button;
position-area: bottom;
position-try-fallbacks: flip-block;
inset: auto;
margin: 0.5rem 0 0;
}
}
A browser without anchor positioning will ignore the unsupported block. The popover still opens and remains usable, but it falls back to its normal position.
Nothing breaks.
When not to use it
Anchor positioning is useful, but it does not replace every positioning library or every piece of JavaScript.
You may still need JavaScript for complex interactions, dynamically generated virtual elements, advanced collision detection, drag-and-drop interfaces, multi-level menus with custom behavior, or components that need to coordinate with application state.
It is also important to remember that anchor positioning only handles layout. It does not automatically make a tooltip accessible, decide when it should open, move keyboard focus, or add the correct ARIA relationships.
Use native HTML features such as popovers and dialogs where possible. Then use anchor positioning to control where those elements appear.
To recap
CSS anchor positioning lets one element position and size itself relative to another element.
The basic relationship is:
.trigger {
anchor-name: --trigger;
}
.panel {
position: absolute;
position-anchor: --trigger;
position-area: bottom;
}
position-area handles common placements. anchor() gives you more exact control. anchor-size() lets the positioned element use the anchor’s dimensions. position-try-fallbacks gives the browser alternative placements when the preferred one would overflow.
Like field-sizing, this is another example of the browser taking over work that previously required JavaScript.
For me, the best part is not only that this removes some JavaScript. It also gives the browser responsibility for something the browser already understands better than our code does: where the elements are, how large they are, and how much room is available.
Tooltips, dropdowns, and popovers still need thoughtful HTML, accessibility, and constraints. But we no longer need to calculate every coordinate ourselves.
And that is a very good improvement.