CSS Anchor Positioning: Auto-Syncing Dropdown & Tooltip Sizes Without JS Using `anchor-size()` and `position-visibility`
If you've ever wired up a ResizeObserver, read element.offsetWidth, and injected it into style.width just to match a dropdown's width to its trigger button — or slapped a requestAnimationFrame poll onto a tooltip that kept floating next to an off-screen anchor — you know the pain. I maintained code like that for a long time, but the CSS Anchor Positioning API being classified as Baseline 2026 has changed a lot of that.
Combining anchor-size() with position-visibility lets you declaratively build UI components that react to anchor dimensions and show or hide themselves based on context — no JavaScript required.
As of January 2026, Chrome 125+, Firefox 147+, and Safari 26+ all support it, covering roughly 91% of global browser traffic. The "browser support is still a problem" worry can be put to rest. This article focuses on two lesser-covered features of the CSS Anchor Positioning API — anchor-size() and position-visibility — and walks through the real problems they solve, with code.
Core Concepts
Prerequisite: anchor-name and position-anchor
Before using anchor-size() and position-visibility, you need to understand how anchor registration and referencing work. The basic structure of CSS Anchor Positioning is: give the reference element a name with anchor-name, then declare that name in the positioned element using position-anchor.
.trigger {
anchor-name: --my-anchor; /* Register as an anchor element */
}
.popup {
position: absolute;
position-anchor: --my-anchor; /* Declare this anchor as the reference */
}On top of this connection, anchor() (position coordinates), anchor-size() (dimensions), and position-visibility (conditional display) are layered. A comprehensive overview of the Anchor Positioning API is available at MDN Using CSS anchor positioning.
What is Baseline 2026? It's a browser compatibility classification used by MDN and the web community. "Baseline 2026" means the feature is stably supported across all major browsers (Chrome, Firefox, Safari, Edge) as of 2026. Reaching this tier is a signal that the feature can be used broadly without polyfills.
anchor-size() — A Function That Exposes Anchor Dimensions as <length> Values
This function, part of the CSS Anchor Positioning API, returns the physical or logical dimensions of an anchor element as a length value. I initially confused it with anchor(), but "position coordinates use anchor(), dimensions use anchor-size()" cleared that up immediately.
It's only valid in a specific set of properties: width, height, min-*, max-*, inset, and margin. Using anchor-size() in inset properties like top or left results in an invalid declaration — something I spent a while debugging the first time.
/* Basic syntax */
anchor-size(<anchor-name>? <anchor-size>, <length-percentage>?)
/* <anchor-size> keywords */
/* width | height | block | inline | self-block | self-inline */The second argument is a fallback value. It applies when the anchor doesn't exist or when the reference is broken. In environments where anchor elements can be unmounted from the DOM — like virtualized lists — this value becomes important.
/* Falls back to 200px if the anchor is missing or the reference breaks */
.dropdown {
width: anchor-size(width, 200px);
}The most interesting characteristic is cross-axis referencing. You can pull a dimension from a different axis — like width: anchor-size(height) — to apply the anchor's height to the target's width. Omitting the argument entirely causes the dimension to be automatically selected based on the axis of the property being set.
/* Omitting the argument → on a width property, automatically references the anchor's width */
.dropdown {
width: anchor-size(); /* = anchor-size(width) */
}position-visibility — Conditional Display Based on Anchor State
This property conditionally hides a positioned element based on the visibility of its anchor element or the overflow state of the target element itself. There are three values, and honestly the differences felt confusing at first. Seeing each one through the scenario it solves makes them click immediately.
| Value | Behavior | When to use |
|---|---|---|
always |
Always visible | Fixed UI, debug labels |
anchors-visible |
Hidden when the anchor completely leaves the viewport or is fully obscured | Tooltips and context menus in scrollable lists |
no-overflow |
Hidden immediately the moment the target starts to overflow its positioning container (or viewport) | Popovers inside fixed-size cards or tables |
The "completely" in
anchors-visible: If the anchor is even 1px within the viewport, the element stays visible. It only hides once the anchor is fully gone, which means smooth transitions without sudden flickering during scroll.
The spec's default is anchors-visible, but given how recently the spec has been revised — including implementations in Chromium-based browsers — there are transitional implementation inconsistencies. For any important component, always declare the value explicitly; it's much safer.
Practical Application
Example 1: A Dropdown Menu That Exactly Matches Its Button's Width
This is the most common real-world scenario. That code that reads the trigger button's offsetWidth and dynamically injects it into the dropdown? Handled entirely in CSS.
<div class="nav-item">
<button class="trigger">Menu</button>
<ul class="dropdown">
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>.trigger {
anchor-name: --dropdown-anchor;
}
.dropdown {
position: absolute;
position-anchor: --dropdown-anchor;
/* Position: directly below the button, left-aligned */
top: anchor(bottom);
left: anchor(left);
/* Size: same width as the button; hidden when the anchor is fully gone */
width: anchor-size(width);
position-visibility: anchors-visible;
}| Property | Role |
|---|---|
anchor-name: --dropdown-anchor |
Registers the button as an anchor |
position-anchor: --dropdown-anchor |
Specifies which anchor the dropdown references |
width: anchor-size(width) |
Inherits the button's width directly |
position-visibility: anchors-visible |
Hides the dropdown when the button is fully scrolled away |
One thing to watch out for: if .dropdown is inside a container with overflow: hidden, the dropdown will be clipped. The common fix is to use the popover attribute or move the dropdown outside the clipping container in the DOM. This is covered a bit more in the drawbacks section.
Example 2: Keeping a Badge Square with Cross-Axis Referencing
When an icon's size changes dynamically and you need a badge to always remain square, cross-axis referencing shines. Read the anchor's height and apply it to both the width and height of the badge simultaneously.
.icon {
anchor-name: --icon;
}
.badge {
position: absolute;
position-anchor: --icon;
top: anchor(top);
right: anchor(right);
translate: 50% -50%;
/* Cross-axis: icon height → applied to both badge width and height */
width: anchor-size(height);
height: anchor-size(height);
border-radius: 50%;
}Cross-axis referencing: The pattern of referencing a dimension from a different axis than the property — like using
anchor-size(height)in awidthproperty. Unlikeanchor(),anchor-size()has no directional constraint.
Example 3: Choosing Between the Three position-visibility Values by Situation
Seeing the code for each scenario directly is the fastest way to internalize which value to reach for.
/* Pattern A: Always visible — fixed instructional label above a sticky header */
.sticky-label {
position-anchor: --sticky-header;
position-visibility: always;
}
/* Pattern B: Hidden with anchor — context tooltip in a scrollable list */
.list-tooltip {
position-anchor: --list-item;
position-visibility: anchors-visible;
/* Tooltip only disappears once the item is fully scrolled out of view */
}
/* Pattern C: Hidden immediately on overflow — popover inside a card */
.card-popover {
position-anchor: --card-trigger;
position-visibility: no-overflow;
/* Hidden the instant it overflows the card boundary by even 1px */
}Example 4: Layered Fallbacks Combined with @position-try
A two-stage fallback pattern: first try flipping the position when space is tight, then hide entirely if flipping doesn't help either.
@position-try --flip-up {
top: auto;
bottom: anchor(top);
}
.tooltip {
position-anchor: --question-mark;
top: anchor(bottom);
/* Stage 1: Flip above if there's no space below */
position-try-fallbacks: --flip-up;
/* Stage 2: If the anchor disappears even after flipping, hide */
position-visibility: anchors-visible;
/* Caps max height at 4× the anchor's height — prevents runaway content length */
max-height: calc(anchor-size(height) * 4);
}Using anchor-size() inside calc() lets you set size constraints relative to the anchor's dimensions. It's a useful pattern whenever you need limits that scale proportionally to the anchor rather than fixed pixel values.
The strength of this pattern is that the browser handles the "adjust position → if that still doesn't work, hide" hierarchy automatically. Logic that would require scroll-event calculations in JS is reduced to a handful of CSS declarations.
Pros and Cons
Saying "zero bundle size" might sound a little sensational, but if you actually remove Floating UI and run a bundle analysis, the numbers do change. Floating UI is already a well-optimized library, but replacing basic use cases with native CSS means the external dependency disappears entirely — which is a meaningful shift from a long-term maintenance perspective.
Advantages
| Item | Details |
|---|---|
| Performance | Processed directly by the browser's layout engine. No getBoundingClientRect() loops or requestAnimationFrame polling. Measurable difference in scroll performance and battery efficiency on low-end mobile |
| Dependency removal | Handles common use cases without Floating UI, Popper.js, or Tippy.js |
| Declarative fallbacks | @position-try handles automatic position flipping on viewport collision; code is significantly simpler |
| Maintainability | Browser automatically responds to scroll, resize, and layout shifts; no JS event listener management needed |
Drawbacks and Caveats
| Item | Details | Mitigation |
|---|---|---|
| Legacy browsers | Not supported before Baseline 2026 (older Safari, Firefox < 147) | Branch with @supports (anchor-name: --x); load Floating UI 3.0 only for legacy |
overflow: hidden containers |
If anchor and target share a clipping container, the target gets clipped | Use the popover attribute or move the element outside the clipping container in the DOM |
| Shadow DOM cross-boundary | Constraints on anchor-target references that cross Shadow DOM boundaries | Use only within the same Shadow root, or supplement with JS |
| Virtualized lists | Anchor may unmount from the DOM, breaking the reference | Specify a fallback value like anchor-size(width, 200px); use JS alongside in virtualized environments |
no-overflow not supported in Top Layer |
no-overflow does not apply to Top Layer elements like <dialog> or popover (W3C issue #10454) |
Substitute with anchors-visible or supplement with JS until resolved |
Top Layer: Elements with
<dialog>or thepopoverattribute are placed by the browser in a special layer rendered above everything else. There is currently an unimplemented issue withno-overflowbehavior in this layer.
The Most Common Real-World Mistakes
- Trusting the
position-visibilitydefault and not declaring it explicitly: Transitional browser implementation inconsistencies have emerged during spec revisions. Always explicitly declare the value you want. - Leaving elements inside an
overflow: hiddenparent: This is the most common blocker. The typical fix for popovers and dropdowns is to use thepopoverattribute or move them outside the clipping container in the DOM structure. - Confusing
anchor()withanchor-size(): Usinganchor-size()in an inset property liketop: anchor-size(height), or usinganchor()in a size property likewidth: anchor(bottom), results in an invalid declaration. "Position coordinates useanchor(), dimensions useanchor-size()" is the distinction to remember.
Closing Thoughts
After actually applying anchor-size() and position-visibility in a project, there's one tangible shift you notice: layout-related JavaScript gradually shrinks. Of course, there are still situations where JS is clearly the better tool — virtualized lists and Shadow DOM boundaries, for example. These two properties don't replace everything, but the range of "turns out CSS can handle this" has gotten quite wide.
Three steps you can try right now:
- Check how Floating UI or Popper.js is being used in your
package.json. Identifying which components rely on them gives you a shortlist of CSS migration candidates. - Take one candidate dropdown or tooltip and write a CSS Anchor Positioning version inside a
@supports (anchor-name: --x) { }block. Supporting browsers will use the new implementation; others will fall back to the existing JS logic — so you can experiment safely. - Try adding
position-visibility: anchors-visibleto a list UI with a scroll container. You can immediately see tooltips naturally disappear as items scroll out of view.
References
- anchor-size() CSS function | MDN Web Docs
- position-visibility CSS property | MDN Web Docs
- Using CSS anchor positioning | MDN Web Docs
- Fallback options and conditional hiding for overflow | MDN Web Docs
- Introducing the CSS anchor positioning API | Chrome for Developers Blog
- The CSS anchor positioning API | Chrome for Developers Docs
- anchor-size() | CSS-Tricks Almanac
- position-visibility | CSS-Tricks Almanac
- CSS Anchor Positioning Guide | CSS-Tricks
- First Public Working Draft: CSS Anchor Positioning Module Level 2 | W3C News
- position-visibility: no-overflow does not support Top Layer elements | w3c/csswg-drafts #10454