A tooltip belongs next to a button. A menu belongs next to its trigger. A validation message belongs next to its input.
The design sounds simple, but the implementation often becomes a small geometry engine: read getBoundingClientRect(), calculate an offset, watch scrolling, handle resizing, detect viewport collisions, and update everything again when the layout changes.
CSS Anchor Positioning gives the browser a declarative way to tether one element to another.
In January 2026, the core anchor() function became Baseline Newly available across the latest major browser versions. The wider module is still evolving, and MDN marks some individual properties as limited availability, so production code still needs feature checks and a fallback.
This guide builds a practical tooltip, explains overflow handling, and shows where JavaScript and accessibility are still required.
What you will learn
- how to tether a floating element to a trigger with CSS
- how to keep a dependable fallback for older browsers
- how anchor positioning works with tooltips and the Popover API
- which accessibility and interaction responsibilities still need attention
The basic mental model
Anchor positioning has three parts:
- Name the anchor element.
- Associate the positioned element with that anchor.
- Place the positioned element relative to an anchor edge.
Here is a small tooltip structure:
<span class="help">
<button
class="help__trigger"
type="button"
aria-describedby="billing-help"
>
What is a billing cycle?
</button>
<span class="help__tooltip" id="billing-help" role="tooltip">
The period covered by one invoice.
</span>
</span>
The wrapper is useful for the fallback. The CSS enhancement names the button as an anchor:
.help__trigger {
anchor-name: --billing-trigger;
}
.help__tooltip {
position: absolute;
position-anchor: --billing-trigger;
top: calc(anchor(bottom) + 0.5rem);
left: anchor(center);
translate: -50% 0;
max-inline-size: 18rem;
padding: 0.65rem 0.75rem;
border-radius: 0.5rem;
background: #111827;
color: white;
box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 20%);
}
The browser now calculates the button's bottom edge and horizontal centre. The tooltip stays tethered even when responsive layout changes move the trigger.
No getBoundingClientRect() loop is needed for the position.
Add a dependable fallback first
New CSS should not turn an unsupported browser into a broken interface.
Start with a classic containing-block fallback:
.help {
position: relative;
display: inline-block;
}
.help__tooltip {
position: absolute;
z-index: 10;
inset-block-start: calc(100% + 0.5rem);
inset-inline-start: 50%;
translate: -50% 0;
}
Then place the anchor-specific rules inside @supports:
@supports (anchor-name: --billing-trigger) {
.help__trigger {
anchor-name: --billing-trigger;
}
.help__tooltip {
position-anchor: --billing-trigger;
top: calc(anchor(bottom) + 0.5rem);
left: anchor(center);
inset-block-start: auto;
inset-inline-start: auto;
}
}
Browsers that do not support anchor positioning keep the wrapper-based layout. Supporting browsers use the anchor.
Feature detection is better than checking a user-agent string because it asks the browser about the capability you need.
Keep the interaction accessible
CSS can calculate position, but position is only one part of a tooltip.
A keyboard user must be able to reveal the content. A touch user cannot depend on hover. The trigger and tooltip need a meaningful relationship.
A simple visual state can start with both hover and focus:
.help__tooltip {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
.help:has(.help__trigger:hover) .help__tooltip,
.help:has(.help__trigger:focus-visible) .help__tooltip {
opacity: 1;
visibility: visible;
}
For short, non-interactive supplementary text, role="tooltip" and aria-describedby can be appropriate.
If the floating content contains buttons, links, form controls, or substantial information, it is not a tooltip. Use a dialog, menu, or popover pattern with correct focus management and keyboard behaviour.
CSS Anchor Positioning does not replace semantic HTML or interaction logic.
Place a popover next to its trigger
Anchor positioning works well with the Popover API because the two features solve different problems:
- the Popover API controls top-layer display and light-dismiss behaviour
- anchor positioning controls geometry
<button
class="account-button"
popovertarget="account-menu"
>
Account
</button>
<div class="account-menu" id="account-menu" popover>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
</div>
.account-button {
anchor-name: --account-button;
}
.account-menu {
position-anchor: --account-button;
position: fixed;
top: calc(anchor(bottom) + 0.5rem);
right: anchor(right);
margin: 0;
min-inline-size: 12rem;
}
Using fixed positioning can be useful for top-layer content, but test the exact combination in the browsers you support. Popover behaviour, anchor association, scrolling, transforms, and containing blocks can interact in ways that are easy to miss in a small demo.
Also choose the correct accessible pattern. A list of normal navigation links does not automatically need ARIA menu roles. Native link and button semantics are often simpler and more robust.
Handle viewport overflow
A tooltip below a trigger can be clipped when the trigger is near the bottom of the viewport.
Anchor positioning includes position-try fallbacks that let the browser test alternate placements.
.help__tooltip {
position-try-fallbacks:
flip-block,
flip-inline,
flip-block flip-inline;
}
The browser can try flipping the block direction, the inline direction, or both when the preferred placement does not fit.
You can also define a custom fallback:
@position-try --above-trigger {
top: auto;
bottom: calc(anchor(top) + 0.5rem);
}
.help__tooltip {
position-try-fallbacks: --above-trigger;
}
Support for the complete position-try feature set may differ from support for the core anchor() function. Check each property you depend on, not just one general compatibility badge.
A practical rollout can therefore have two levels:
- anchor positioning for the main placement
- your existing overflow logic retained until the required position-try features meet your support target
Progressive enhancement does not need to be all or nothing.
Repeated components need anchor scoping
Imagine a product list with 20 help buttons. If every button uses the same anchor name, an unscoped positioned element can associate with the last matching anchor in source order.
The anchor-scope property is designed to limit an anchor name to a subtree.
.product-card {
anchor-scope: --details-trigger;
}
.product-card__button {
anchor-name: --details-trigger;
}
.product-card__panel {
position-anchor: --details-trigger;
}
This lets repeated components reuse a meaningful anchor name without all panels attaching to one button.
Because anchor-scope is newer than the basic technique, verify its current compatibility. Until it fits your browser policy, unique generated anchor names are a reasonable fallback for component libraries.
When anchor() is better than position-area
The anchor() function works inside inset properties such as top, right, bottom, and left. It is explicit and combines naturally with calc().
.notification {
left: calc(anchor(right) + 12px);
top: anchor(center);
translate: 0 -50%;
}
The position-area property provides a higher-level grid-like placement vocabulary.
.notification {
position-area: center right;
}
position-area can be concise, while anchor() gives precise control. They are complementary. Choose the smallest feature set that expresses your layout and is supported by your target browsers.
What this replaces—and what it does not
Anchor positioning can replace a lot of coordinate calculation for:
- tooltips
- dropdown panels
- contextual help
- callouts
- labels attached to controls
- floating action panels
- autocomplete surfaces
It does not automatically provide:
- show and hide state
- focus management
- Escape-key handling
- click-outside behaviour
- accessible roles and labels
- collision support in every target browser
- application-specific placement policy
Libraries such as Floating UI still provide value when you need mature cross-browser collision handling, virtual anchors, framework integration, or one abstraction across older browsers.
The new CSS lets you reduce that dependency when your use case and browser matrix allow it.
A safe migration plan
Step 1: Measure the current complexity
Find components that read layout coordinates, subscribe to scroll and resize events, or repeatedly update inline top and left styles.
Step 2: Choose one low-risk component
A non-interactive tooltip or simple contextual label is easier to migrate than a complex editor menu.
Step 3: Keep the current fallback
Write the old layout as the default and add the anchor enhancement inside @supports.
Step 4: Test real layout conditions
Test:
- viewport edges
- zoom at 200%
- long translated text
- right-to-left layout
- nested scrolling containers
- transforms on ancestors
- mobile touch interaction
- keyboard navigation
- the browser versions in your support policy
Step 5: Measure before deleting JavaScript
If the CSS path works across your required conditions, remove only the geometry code it actually replaces. Keep state and accessibility logic that is still necessary.
Common mistakes
Making the tooltip visible only on hover
Hover does not cover keyboard or touch interaction. Include focus behaviour and choose a suitable disclosure pattern for mobile.
Using a tooltip for interactive content
A tooltip should not contain buttons or form fields. Use a popover, dialog, or other interactive pattern.
Assuming one support badge covers the whole module
Core anchor() support and newer properties such as anchor-scope or some position-try features can have different compatibility states.
Forgetting the fallback
Unsupported CSS is ignored. If the anchor rules are the only placement rules, the element may appear in the wrong location.
Replacing tested code too early
The goal is not to delete a library because a demo works. The goal is to simplify a real component without losing behaviour.
Conclusion: let CSS own the geometry
Floating interfaces have historically asked JavaScript to measure the layout and then tell CSS where to draw.
Anchor positioning gives more of that work back to the browser's layout engine.
That can mean less code, fewer scroll and resize listeners, and a more direct relationship between the trigger and the floating element. The win is strongest when it is combined with semantic HTML, accessible state handling, honest compatibility checks, and a tested fallback.
Start with one tooltip. Keep the old path. Let the browser handle the geometry where it can.
Top comments (0)