Delete Your Tooltip Library: CSS Anchor Positioning and Popover Do It Now
Anchor positioning now works in every major browser. Build dropdowns and tooltips that position and flip themselves, with almost no JavaScript.
CarnielOn this page9 sections
For years, a dropdown that stays attached to its button meant reaching for a library. Floating UI, Popper, or a component kit that bundles one of them. You measured the button, measured the menu, did some maths, then listened to scroll and resize so it didn't drift.
That's over. CSS anchor positioning landed in Firefox 147 in January 2026, which made it Baseline across Chrome, Edge, Safari and Firefox (web.dev). Pair it with the Popover API, which has been Baseline since January 2025 (web-features), and you can build menus and tooltips that position themselves, flip when they run out of room, close on Escape and sit above everything else, with almost no JavaScript.
For a sense of what you're replacing: I bundled the usual Floating UI setup (computePosition, flip, shift, offset and autoUpdate from @floating-ui/dom 1.8.0) with esbuild. It came out at 15.8 KB minified, 6.3 KB gzipped, before any of your own code. The CSS version is a handful of lines in a stylesheet you already ship.
The two pieces
Popover handles behaviour. An element with the popover attribute is hidden by default, opens when a button points at it, renders in the top layer so you never fight z-index again, and closes when you click outside or press Escape.
Anchor positioning handles placement. You give one element a name with anchor-name, point another at it with position-anchor, and say where it should go with position-area.
Neither needs the other, but together they cover most of what people install a positioning library for.
A dropdown menu with zero JavaScript
Start with the HTML. The button's popovertarget points at the menu's id. That's all the wiring you need to open and close it:
<button class="account-trigger" popovertarget="account-menu">
Account
</button>
<div id="account-menu" class="account-menu" popover>
<a href="/profile">Profile</a>
<a href="/settings">Settings</a>
<button>Sign out</button>
</div>Then the CSS:
.account-trigger {
anchor-name: --account-menu;
}
.account-menu {
position-anchor: --account-menu;
position-area: bottom span-right;
margin: 0;
margin-top: 8px;
}The menu now opens directly below the button, lined up with its left edge, with an 8px gap. Scroll the page and it stays attached. No listeners, no getBoundingClientRect.
The one gotcha that will cost you an hour: popovers come with
margin: autofrom the browser so they centre on screen by default. If you forgetmargin: 0, your menu will float off somewhere in the middle of the page and ignoreposition-area. I tested this in Chromium: without the reset, the menu landed about 340px away from its button.
How position-area thinks
position-area imagines a 3x3 grid around the anchor, with the anchor in the middle cell. You pick a cell, or a span of cells, and the popover is placed there.
A few values you'll use all the time:
top: centred above the anchor. Good for tooltips.bottom span-right: below, starting at the anchor's left edge and growing right. Good for menus.bottom span-left: the same, but aligned to the right edge. Good for an avatar menu in the top-right corner.right: beside the anchor, centred vertically. Good for nested submenus.
Flip when there's no room
The main reason people used Floating UI was flip: if the menu would fall off the bottom of the screen, show it above instead. CSS has that built in:
.account-menu {
position-anchor: --account-menu;
position-area: bottom span-right;
position-try-fallbacks: flip-block, flip-inline;
margin: 0;
margin-top: 8px;
}flip-block tries the opposite side vertically, and flip-inline tries the opposite side horizontally. The browser picks the first option that fits. It flips the margin too, so your 8px gap stays between the menu and the button when it opens upwards. In my test, a button sitting near the bottom of the viewport opened its menu above, with the 8px gap intact.
If the built-in flips aren't enough, you can write your own fallback positions with @position-try:
@position-try --beside {
position-area: right span-bottom;
margin: 0 0 0 8px;
}
.account-menu {
position-try-fallbacks: flip-block, --beside;
}Match the button's width
For select-style dropdowns, you usually want the list to be at least as wide as the button. That used to be another measurement in JavaScript. Now it's one line:
.account-menu {
min-width: anchor-size(width);
}Tooltips
Tooltips need one thing the dropdown didn't: they open on hover and focus, not on click. HTML is getting a way to do that without JavaScript, the interestfor attribute, but right now only Chrome and Edge support it (web-features). Until the others catch up, four event listeners do the job:
<button class="copy-btn" aria-describedby="copy-tip">Copy</button>
<div id="copy-tip" class="tooltip" popover="manual" role="tooltip">
Copy to clipboard
</div>.copy-btn {
anchor-name: --copy;
}
.tooltip {
position-anchor: --copy;
position-area: top;
position-try-fallbacks: flip-block;
margin: 0 0 6px;
padding: 4px 8px;
border: 0;
border-radius: 6px;
background: #111;
color: #fff;
}const btn = document.querySelector('.copy-btn')
const tip = document.getElementById('copy-tip')
const show = () => tip.showPopover()
const hide = () => tip.hidePopover()
btn.addEventListener('pointerenter', show)
btn.addEventListener('pointerleave', hide)
btn.addEventListener('focus', show)
btn.addEventListener('blur', hide)A few details matter here:
popover="manual"means the tooltip won't light-dismiss or close other popovers when it opens. You control it fully. There's alsopopover="hint", made for tooltips, but Safari doesn't support it yet (web-features).aria-describedbyandrole="tooltip"let screen readers announce the text when the button gets focus.Focus as well as hover. Keyboard users never hover. If your tooltip only appears on
pointerenter, they never see it.Keep tooltips plain. No links or buttons inside. If it needs to be interactive, it's a popover menu, not a tooltip.
Using it in React and Next.js
Anchor names have to be unique on the page. If you render the same tooltip 20 times with --copy, every tooltip will attach to the last button. In React, useId solves that:
'use client'
import { useId, useRef, type ReactNode } from 'react'
type TooltipProps = {
label: string
children: ReactNode
}
export function Tooltip({ label, children }: TooltipProps) {
const id = useId()
const anchor = `--tip${id.replace(/[^a-zA-Z0-9_-]/g, '')}`
const tipRef = useRef<HTMLDivElement>(null)
const show = () => tipRef.current?.showPopover()
const hide = () => tipRef.current?.hidePopover()
return (
<>
<span
style={{ anchorName: anchor }}
aria-describedby={id}
onPointerEnter={show}
onPointerLeave={hide}
onFocus={show}
onBlur={hide}
>
{children}
</span>
<div
ref={tipRef}
id={id}
role="tooltip"
popover="manual"
className="tooltip"
style={{ positionAnchor: anchor }}
>
{label}
</div>
</>
)
}Then <Tooltip label="Copy to clipboard"><button>Copy</button></Tooltip> anywhere.
Two things worth knowing. First, the replace is there because older React versions returned ids like :r0:, and colons aren't allowed in CSS names. React 19.3 returns _R_0_, which is fine, but the clean-up costs nothing and protects you if the format changes again. Second, React passes anchorName and positionAnchor through as normal style properties, and the types already know about them, so no casting is needed. I checked this component with TypeScript in strict mode and in Chromium: each tooltip opened above its own button.
If you're on Tailwind v4, you can set these with arbitrary properties like [anchor-name:--account-menu] and [position-area:bottom_span-right], or keep them in a small CSS file next to the component.
When to keep Floating UI
Native CSS doesn't cover everything yet. Keep the library, or load it only when needed, if you have:
Users on older browsers. Anchor positioning needs Chrome 125+, Safari 26+ or Firefox 147+. If your analytics show lots of older iPhones that haven't updated, check before you delete anything.
Virtual anchors. Positioning a menu at the mouse cursor or a text selection needs an anchor element. Floating UI can position against any rectangle you give it.
Arrows that track the anchor. A tooltip arrow that points at the centre of the button even after the tooltip shifts is still easier with Floating UI's
arrowmiddleware.
For the older-browser case, you don't have to choose one or the other. Check for support and only load the library when the browser needs it:
if (!CSS.supports('anchor-name: --a')) {
const { computePosition, flip, offset } = await import('@floating-ui/dom')
// position menus the old way
}Most of your users get zero kilobytes. The rest get the same experience they have today.
Try it on one component
Don't rip out every dropdown at once. Pick one, maybe the account menu in your header, and rebuild it with popover and position-area. Check it near the edges of the screen, with the keyboard, and on your phone. Once it holds up, the rest of your floating UI will follow quickly.
If you like replacing JavaScript with platform features, my post on React ViewTransition in Next.js does the same for page transitions.