Calculating Tooltip and Dropdown Positions Without JavaScript Using CSS Anchor Positioning
Have you ever repeatedly called getBoundingClientRect() to calculate tooltip positions? A few years ago, while building an onboarding coach mark UI, I introduced Floating UI (a JS-based positioning library), set up a requestAnimationFrame loop to update coordinates, and spent days tracking down a bug where positions would jump on scroll. Back then, I thought "wouldn't it be great if CSS could handle this?" — and now it really can.
CSS Anchor Positioning is a CSS feature that declaratively links one element to another to determine its position. The browser layout engine handles coordinate calculations, and viewport boundary handling is done in a single line of CSS. Chrome was first to ship support in 2024, and by January 2026, Firefox 147 followed, with Safari 26 joining shortly after, bringing it to Baseline 2026. Baseline means all major browsers have implemented the feature and interoperability is established — global support per Can I Use is approximately 76%. It's time to seriously consider this for production use.
This article covers everything in one place: the core concepts of CSS Anchor Positioning, practical patterns, polyfill strategies, and edge cases where JS is still needed.
Core Concepts
Why Positioning Was Difficult with the Old Approach
Let's start with the limitations of the old approach. The most troublesome issues when building tooltips or dropdowns were overflow: hidden and z-index stacking context problems. A stacking context is the unit by which browsers determine the order in which elements are stacked along the Z-axis — when properties like transform or opacity create a new context, child elements can no longer visually escape the parent boundary. Placing a positioned element inside a parent causes it to be clipped by overflow, but moving it under <body> makes the DOM messy and requires separate scroll synchronization handling.
CSS Anchor Positioning takes a fundamentally different approach to this problem. Regardless of where the anchor element and the positioned element are in the DOM tree, they can be connected by CSS name alone, and coordinate calculations are handled by the browser layout engine.
Three Requirements for Anchor Positioning to Work
Three things must be in place for anchor positioning to function.
/* 1. The element to register as an anchor */
.trigger {
anchor-name: --my-trigger; /* dashed ident format required */
}
/* 2. The positioned element connected to the anchor */
.tooltip {
position: absolute; /* positioning context */
position-anchor: --my-trigger; /* specify the anchor to reference */
position-area: top center; /* place at top center of the anchor */
}- Association: Register an element as an anchor with
anchor-nameand reference it withposition-anchor - Positioning context: Apply
position: absoluteorposition: fixedto the positioned element - Location: Determine the position relative to the anchor with
position-areaor theanchor()function
anchor-namevalues must be in dashed ident format starting with--. This follows the same naming convention as CSS custom properties — forms like--my-anchorand--dropdown-triggerare correct. Writing it asmy-anchorwill silently fail with no error, making debugging quite difficult.
Core Properties at a Glance
Here are the properties you'll find yourself looking up most often when working with this directly.
| Property/Function | Role |
|---|---|
anchor-name |
Register an element as an anchor |
position-anchor |
Specify the anchor to reference |
position-area |
Position using a 9-cell grid |
anchor() |
Use a specific anchor edge for inset calculations |
anchor-size() |
Set the positioned element's size based on the anchor's width/height |
position-try-fallbacks |
List of fallback positions to try automatically when the viewport is exceeded |
anchor-scope |
Limit the anchor name's scope to a subtree |
anchor-scope is especially important in repeating components where each list item has its own tooltip. When reusing the same anchor-name across multiple components, omitting anchor-scope causes the entire page to reference only the last declared anchor of that name. Declaring anchor-scope: --my-tooltip on each component root restricts that anchor name to be valid only within that subtree.
position-area: Determining Position with a 9-Cell Grid
position-area is easiest to understand by imagining a 3×3 grid centered on the anchor. Combine horizontal and vertical axis keywords to specify the desired cell.
position-area: top center; /* top center of the anchor */
position-area: bottom span-left; /* below the anchor, extending left */
position-area: right span-all; /* right of the anchor, spanning full height */The span- keyword is used to extend the area to adjacent cells. Logical keywords like inline-start and self-start are also available, which are useful for RTL (right-to-left) layouts and multilingual writing directions. In environments where the writing direction is reversed, such as Arabic or Hebrew, using logical keywords instead of left/right will automatically flip the layout.
Automatic Viewport Overflow Handling
This is personally my favorite feature. Previously, you had to manually calculate viewport boundaries on every scroll event to switch positions — now you just list candidate positions in position-try-fallbacks and the browser automatically tries them in order, selecting the optimal position.
.tooltip {
position: absolute;
position-anchor: --trigger;
position-area: top center;
position-try-fallbacks: bottom center, right span-top, left span-top;
/* If top doesn't work, try bottom, then right, then left */
}Practical Application
Example 1: A :hover Tooltip That Works Right Now
I originally intended to show an example using the interestfor attribute first, but it's still experimental in most browsers, so copying and pasting it won't work. I decided to lead with the version you can use right now.
Wrapping with @supports means that in browsers without anchor positioning support, the tooltip is simply hidden, and positioning only applies in browsers that support it.
<div class="tooltip-wrapper">
<button class="trigger">View Help</button>
<div class="tooltip" role="tooltip">You can drag and drop files to upload.</div>
</div>.trigger {
anchor-name: --btn-trigger;
}
.tooltip {
display: none; /* default: hidden */
}
/* Positioning only in browsers that support anchor positioning */
@supports (anchor-name: --x) {
.tooltip {
display: block;
opacity: 0;
pointer-events: none;
transition: opacity 0.15s;
position: absolute;
position-anchor: --btn-trigger;
position-area: top center;
position-try-fallbacks: bottom center, right span-top, left span-top;
background: #1a1a2e;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.875rem;
max-width: 200px;
}
.trigger:hover + .tooltip,
.trigger:focus-visible + .tooltip {
opacity: 1;
}
}| Code | Role |
|---|---|
@supports (anchor-name: --x) |
Progressive enhancement — hides tooltip in unsupported browsers |
position-area: top center |
Places element above and centered on the button |
position-try-fallbacks |
Tries fallback positions in order when top space is insufficient |
.trigger:hover + .tooltip |
Controls hover state using CSS adjacent sibling selector |
Using the
interestforattribute allows hover/focus triggers to be declared directly in HTML, making the CSS cleaner as well. However, since it's currently an experimental feature at the Chrome Canary level, it's recommended to watch the Interop 2026 progress rather than use it right away.
Example 2: Dropdown Menu (Automatically Matching Button Width)
Dropdown menus look natural when they maintain the same width as the button. The anchor-size() function handles this in CSS as well.
<div class="menu-wrapper">
<button class="menu-button" popovertarget="dropdown">Open Menu</button>
<ul id="dropdown" popover class="dropdown-panel">
<li>Profile Settings</li>
<li>Notification Management</li>
<li>Sign Out</li>
</ul>
</div>.menu-button {
anchor-name: --menu-trigger;
}
.dropdown-panel {
position: absolute;
position-anchor: --menu-trigger;
position-area: bottom span-right;
min-width: anchor-size(width); /* automatically set to at least the button's width */
position-try-fallbacks: top span-right; /* flip to top if there's not enough space below */
}anchor-size(width) references the anchor element's current width in real time. In responsive layouts, even if the button width changes, the dropdown follows automatically. Previously, this required reading the button width with JavaScript and setting it directly on the dropdown.
Example 3: Onboarding Coach Marks (Replacing 200 Lines of JS with a Few Lines of CSS)
Honestly, this was a bit of a shock when I first saw it. Just the logic to calculate the coordinates of the highlighted target element, account for scroll position, and respond to resize events was easily over 100 lines.
<button class="feature-button">New Feature</button>
<div class="coach-mark">Try clicking this button!</div>.feature-button {
anchor-name: --feature-highlight;
}
.coach-mark {
position: absolute;
position-anchor: --feature-highlight;
position-area: top center;
position-try-fallbacks: bottom center, right span-top, left span-top;
/* Speech bubble styling */
background: #fff;
border: 2px solid #6366f1;
border-radius: 8px;
padding: 10px 14px;
max-width: 220px;
font-size: 0.875rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
/* Speech bubble tail — downward arrow when coach mark is above the anchor */
.coach-mark::after {
content: "";
position: absolute;
top: 100%; /* tail at the bottom of the speech bubble */
left: 50%;
transform: translateX(-50%);
border: 6px solid transparent;
border-top-color: #6366f1;
}The browser handles viewport boundary handling, scroll response, and resize response. You only need to style the speech bubble tail (::after) yourself. If you want the tail direction to also change when the position switches between top and bottom via position-try-fallbacks, you can make use of @position-try rules.
Example 4: Autocomplete Dropdown (Snapping to the Bottom of the Input Field)
This is the pattern of attaching a suggestion list below a search field or address input.
.search-input {
anchor-name: --search-anchor;
width: 100%;
}
.suggestions-list {
position: absolute;
position-anchor: --search-anchor;
position-area: bottom span-all; /* positioned below, matching the full width of the input */
max-height: 300px;
overflow-y: auto;
}Thanks to the span-all keyword, the suggestion list aligns exactly with the input field's width. Previously, this required synchronizing the width with JavaScript.
Pros and Cons
Advantages
Listed in order of what I found most impactful when using it directly.
| Item | Details |
|---|---|
| Performance | The browser layout engine handles coordinate calculations. requestAnimationFrame loops and getBoundingClientRect() calls disappear |
| DOM structure freedom | Anchor and positioned elements can reference each other regardless of where they are in the DOM. Resolves z-index/overflow stacking context issues |
| Automatic viewport handling | The browser automatically selects the optimal position just by declaring position-try-fallbacks |
| Bundle size reduction | Can eliminate dependency on JS-based positioning libraries like Floating UI and Popper.js |
| Code conciseness | Concentrating positioning logic in CSS improves maintainability |
Disadvantages and Caveats
Progressive Enhancement is a strategy where you ensure basic functionality works in all browsers, then provide a better experience in environments that support modern features. The representative approach is to check for support with an
@supports (anchor-name: --x) { }query and branch the CSS accordingly — declaring a fallback as in Example 1's code means basic functionality still works in unsupported browsers.
An honest summary of limitations and workarounds encountered in real-world use.
| Item | Details | Workaround |
|---|---|---|
| No support in legacy browsers | ~76% global support, excluding older browsers | Apply OddBird polyfill or use progressive enhancement strategy |
| Polyfill limitations | position-area adds a wrapping element that can break CSS selectors |
UIs relying on ~, +, >, :nth-child selectors need separate verification |
| External stylesheets not parsed | Polyfill cannot parse position-area in imported external CSS |
Include core positioning CSS inline or in the main bundle |
| Limits for complex cases | Shadow DOM cross-origin, virtualized lists, multi-level nested menus | Recommend keeping JS libraries like Floating UI for these cases |
position-area keyword complexity |
Many logical keywords such as span-all, self-start, inline-start |
Use MDN reference and browser DevTools |
The Most Common Mistakes in Practice
- Writing
anchor-namevalues without a dashed ident — It must be--my-anchor, notmy-anchor. Missing this rule causes silent failure with no error, making debugging quite difficult. - Omitting
position: absoluteorposition: fixedon the positioned element —position-anchorandposition-arearequire a positioning context. Without it, they are also silently ignored. - Not being able to find the cause when adjacent selectors behave unexpectedly in a polyfill environment — The OddBird polyfill has the side effect of inserting a wrapping element to handle
position-area. If~or:nth-childselectors are behaving differently than expected, a polyfill issue should be your first suspicion.
Closing Thoughts
CSS Anchor Positioning is a structural shift that brings positioning calculation logic — long handled by JavaScript — into the browser layout engine. For patterns where "one element needs to attach next to another element," such as tooltips, dropdowns, and popovers, you can experience a simultaneous reduction in both JS code volume and bundle size.
Since support is still at 76%, rather than switching unconditionally, it's recommended to gradually introduce it starting with newly created components. There's no need to immediately rip out Floating UI or Popper.js from existing projects, but for new development, try implementing with CSS Anchor Positioning first and experience the code reduction firsthand.
Three steps to get started right now:
- Check browser support first — If you're using Chrome 125+, Firefox 147+, or Safari 26+, you can try it immediately. Open the live demo from MDN's CSS anchor positioning guide to get a feel for it quickly.
- Pick an existing tooltip and port it — Select the simplest tooltip component in your project and try reimplementing it with just three things:
anchor-name+position-anchor+position-area. You'll immediately feel how much JavaScript code is eliminated. - Prepare the polyfill — If you need to support legacy browsers, install the
@oddbird/css-anchor-positioningpackage and set up a progressive enhancement structure with@supportsqueries. It's a good idea to check the GitHub issues for the latest status on the polyfill's selector limitations.
References
- CSS anchor positioning | MDN Web Docs
- Anchor positioning | web.dev
- CSS Anchor Positioning Guide | CSS-Tricks
- Introducing the CSS anchor positioning API | Chrome for Developers
- CSS Anchor Positioning Module Level 1 | W3C
- First Public Working Draft: CSS Anchor Positioning Module Level 2 | W3C
- Anchor Positioning Updates for Fall 2025 | OddBird
- OddBird CSS Anchor Positioning Polyfill | GitHub
- CSS Anchor Positioning | Can I use
- Updates to Popover and CSS Anchor Positioning Polyfills | OddBird