CSS Anchor Positioning Makes Tooltip Libraries Optional

Jul 21, 2026

Build a dropdown menu and a hover tooltip with CSS anchor positioning and the Popover API, replacing Floating UI with pure browser layout.

For twenty years, positioning a dropdown next to the button that opened it meant JavaScript: measure the button with getBoundingClientRect(), do arithmetic, write inline styles, and re-run all of it on every scroll and resize. That is the entire reason Popper.js and Floating UI exist, and why they still pull millions of npm downloads every week. CSS anchor positioning moves that whole job into the layout engine. You name an element with anchor-name, point another element at it with position-anchor, and the browser tethers them during layout, with declarative fallback rules for when the tethered element would overflow the viewport. Chrome shipped it in Chrome 125 back in May 2024, Safari 26 added it in September 2025, and Firefox is the last engine still working on its implementation, so per Can I Use data it already covers the large majority of global traffic and degrades cleanly where it is missing. This tutorial builds a complete dropdown menu and a hover tooltip in one HTML file with zero JavaScript, using anchor-name, position-area, anchor-size(), position-try-fallbacks, @position-try, and position-visibility along the way.

index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Anchored menus, zero JS</title>
</head>
<body>
  <button id="save-btn" popovertarget="save-menu">
    Save &#9662;
  </button>

  <div id="save-menu" popover>
    <button>Save draft</button>
    <button>Save and publish</button>
    <button>Export as PDF</button>
  </div>
</body>
</html>
index.html

Start with a popover, not a positioning hack

The build starts with the Popover API because it solves half of the dropdown problem already. The popover attribute on the div and popovertarget on the button give you open and close toggling, light dismiss on outside click, Esc to close, and rendering in the top layer above every stacking context. No JavaScript, and it is Baseline in all three engines. What it does not give you is any spatial relationship: the menu has no idea where its button is. That gap is exactly what anchor positioning was designed to fill.

Style it and watch the menu ignore its button

Add basic page and menu styles, then open the file and click Save. The menu appears dead center in the viewport. That is the user-agent stylesheet at work: popovers ship with position: fixed, inset: 0, and margin: auto, which centers them because the browser has nothing better to do with them. This default is the design problem in miniature. The top layer deliberately escapes every ancestor's stacking and clipping context, which is great for z-index sanity but severs any layout relationship with the button. Until now, restoring that relationship meant a resize-observer-and-math library.

Declare the relationship: anchor-name and position-anchor

Two properties create the tether. anchor-name: --save on the button registers it as an anchor; the value is a dashed ident you invent, scoped like a custom property. position-anchor: --save on the menu points at it. Reload and click: nothing moves yet, and that is by design. These properties only establish which element the menu measures against. The spec splits *what am I anchored to* from *where do I go* so that one anchor can serve many positioned elements and the positioning rules stay swappable. The actual placement comes next.

Pin edges with the anchor() function

The low-level primitive is anchor(). Used inside an inset property, it resolves to a coordinate on the anchor's box: top: anchor(bottom) pins the menu's top edge to the button's bottom edge, and left: anchor(left) lines up their left edges. Note the two resets: margin: 0 and inset: auto undo the popover UA styles from the previous step, otherwise the leftover margin: auto keeps fighting you. This is the first popover-plus-anchor gotcha everyone hits, so it is worth internalizing now. Reload, click, and the menu snaps under the button, even though one lives in normal flow and the other in the top layer.

Swap to position-area, the ergonomic layer

Explicit anchor() insets are precise but verbose, so the spec layers position-area on top. Mentally overlay a 3x3 grid on the page with the anchor in the center cell; position-area names which cells the positioned element occupies. bottom span-right means the row below the anchor, starting at the anchor's left edge and allowed to grow rightward, which is exactly a left-aligned dropdown. Two top/left lines become one declaration, and unlike raw insets, position-area values participate in the automatic flipping you will add in a later step. Reach for anchor() only when the grid vocabulary cannot express what you need.

Add a gap and match widths with anchor-size()

Two polish details that used to be library options. The gap between button and menu is just margin: 6px 0 0; because position-area established the containing region, plain margins offset within it, no offset middleware required. Then min-width: anchor-size(width) makes the menu at least as wide as its button. anchor-size() is the sibling of anchor(): it resolves to a dimension of the anchor's box instead of an edge coordinate, and you can use it inside calc(). Matching a menu to its trigger's width previously meant a ResizeObserver and a style write; here it is one declaration the layout engine keeps true forever.

Flip at the viewport edge with position-try-fallbacks

Stretch the page to 180vh so you can scroll, then add position-try-fallbacks: flip-block. Now scroll until the button sits near the bottom of the viewport and open the menu: it renders above the button instead of getting clipped. This is the feature that killed the last argument for a JS library. Floating UI's flip middleware does the same thing by listening to scroll events and re-measuring after layout, one frame late by construction. position-try-fallbacks tells the browser: if the default position overflows, try this transformed position during the same layout pass. No listeners, no jank, no frame lag.

Author your own fallback with @position-try

Canned tactics like flip-block mirror the existing declarations, but sometimes the flipped layout needs different styles entirely. @position-try defines a named alternative: a rule block of position-area, insets, margins, and sizing that the browser applies wholesale when it reaches that fallback. Here --above moves the menu to top span-right and swaps the 6px gap to margin-bottom so the spacing stays correct in the flipped orientation. Fallbacks in position-try-fallbacks are tried strictly in order after the base position fails, so you can chain a custom rule, then flip-inline, then a squeezed last resort.

Anchor a tooltip: no popover needed

Anchoring is not a popover feature; it works on any absolutely or fixed positioned element. Add a Help button and a tooltip div inside a small .toolbar flex wrapper, wire them for screen readers with aria-describedby and role="tooltip", and give the tip position: fixed plus its own position-anchor and position-area: top. It renders centered above the button because top with no span- keyword occupies just the cell directly above, centered on the anchor. One page, two anchors, two positioned elements, and the naming scheme keeps every relationship explicit in the stylesheet.

Reveal on hover and hide with position-visibility

Finish the tooltip: hide it by default with opacity: 0 and pointer-events: none, then reveal it from :hover and :focus-visible on the button, so keyboard users get it too. The last line is the sleeper feature: position-visibility: anchors-visible tells the browser to hide the tooltip whenever its anchor is scrolled out of view. Because the tip is position: fixed, it would otherwise float in space after the button leaves the viewport, a bug class Floating UI handles with its hide middleware and yet another scroll listener. Here it is one declaration, enforced by the compositor. Scroll the toolbar away while hovering and watch it vanish.

One HTML file, no script tag, and you have a light-dismissing dropdown that flips when it runs out of room plus a keyboard-accessible tooltip that hides itself when its anchor scrolls away. Every piece of this used to be a JavaScript library's job. The architectural win is where the work happens: Floating UI recalculates position after layout, from scroll and resize listeners, which is why tethered menus lag a frame behind their buttons on slow devices. Anchor positioning resolves during layout itself, so the menu and button move as one. For production, decide your fallback posture: because the Popover API is Baseline on its own, browsers without anchor support still get a working menu, just centered in the viewport, and you can wrap the anchoring rules in @supports (anchor-name: --a) if you want to swap in a simpler layout instead. Keep Floating UI where you need virtual anchors (positioning against a text selection or a canvas point) or guaranteed pixel-identical behavior in every browser today. For the standard button-plus-menu case, the platform now does this natively, and it does it better.

#code#tutorials