Popover Anchor Targeting: CSS Positioning for Native Tooltips
CSS anchor positioning and the Popover API finally let you build tooltips without JavaScript logic. Here's how to wire them up properly in React and Tailwind v4.
Why Native Popovers Are Worth Your Attention
Honestly, most tooltip libraries you're using right now are solving a problem that the browser already solved — you just don't know it yet. The Popover API shipped in Chrome 114, Safari 17, and Firefox 125. It's available in Baseline 2024. That means you can retire the JavaScript positioning logic you've been carrying around for years.
The old pattern was painful. You'd import Floating UI or Popper.js, calculate getBoundingClientRect(), track scroll events, and pray that z-index didn't betray you. Then you'd do it again every time your design system needed a new tooltip variant. Every. Single. Time.
The Popover API gives you a popover attribute that makes an element live in the top layer — above everything else, including fixed-position navbars and sticky headers. No z-index wars. But the real unlock came with CSS Anchor Positioning, which shipped behind a flag in Chrome 125 and is now in Baseline 2025. Together, they let you declare a positional relationship between a trigger and a floating element in pure CSS.
How CSS Anchor Positioning Actually Works
The anchor positioning spec introduces three core concepts: anchor-name, position-anchor, and the anchor() function. You assign a named anchor to the trigger element. The popover then references that name to calculate its own position. No scroll listeners. No resize observers. The browser handles it.
Here's the minimal setup. You give the button an anchor-name, point the popover at it with position-anchor, then use anchor() to place the popover relative to the trigger's edges:
/* Tailwind doesn't cover anchor positioning yet in v4.0.2,
so you write these in a global CSS layer or a style tag */
.trigger {
anchor-name: --my-tooltip-anchor;
}
.popover-hint {
position: absolute;
position-anchor: --my-tooltip-anchor;
/* Position below the trigger with 8px gap */
top: calc(anchor(bottom) + 8px);
left: anchor(center);
translate: -50% 0;
/* Top-layer popovers need this to allow CSS positioning */
position-try-fallbacks: --flip-above;
}
@position-try --flip-above {
top: auto;
bottom: calc(anchor(top) + 8px);
}The @position-try at-rule is the bit that actually replaces Floating UI's flip middleware. You define one or more fallback placement rules. The browser tries them in order until one fits inside the viewport. That 8px gap in the calc above is your starting point — adjust to taste based on your design tokens.
Wiring It Up in React with the popover Attribute
React 19 ships full support for the popover attribute on HTML elements, so you don't need any workarounds. In React 18 you'd have gotten a warning because popover wasn't a recognized DOM attribute. Now it's clean.
Here's a component that ties together the Popover API and CSS anchor positioning. It avoids any JavaScript positioning logic:
// HintPopover.tsx
interface HintPopoverProps {
id: string;
label: string;
hint: string;
}
export function HintPopover({ id, label, hint }: HintPopoverProps) {
const anchorName = `--anchor-${id}`;
return (
<>
<button
popovertarget={id}
style={{ anchorName }}
className="px-3 py-1.5 rounded-md bg-zinc-800 text-zinc-100
text-sm border border-zinc-700 hover:bg-zinc-700
transition-colors"
>
{label}
</button>
<div
id={id}
popover="hint"
style={{ positionAnchor: anchorName }}
className="
hint-popover
absolute m-0 px-3 py-2 rounded-lg
bg-zinc-900 border border-zinc-700
text-xs text-zinc-200
shadow-lg shadow-black/40
max-w-[240px]
"
>
{hint}
</div>
</>
);
}A few things worth noting. The popover="hint" value is subtly different from popover="auto". A hint popover closes when you interact with anything outside it, but it doesn't close other open popovers. auto popovers form a stack and dismiss each other. Use hint for tooltips. Use auto for dropdowns and menus. The popovertarget attribute wires the button to the popover by ID — no useState, no onClick handler.
Tailwind v4 and Anchor Positioning: What's Missing
Tailwind v4.0.2 doesn't have utility classes for anchor positioning yet. The spec is still stabilizing and browser support is incomplete — anchor-name and position-anchor are Chrome/Edge-only as of late 2026. Firefox has it behind a flag. Safari has it in Technology Preview.
For now, you have two reasonable options. First, write the anchor positioning rules in a @layer components block inside your global CSS file. Second, pass them as inline styles via React's style prop, since CSS custom properties and most anchor positioning values work fine there. The style={{ anchorName: '--my-anchor' }} pattern is clean enough for a component API.
If you're using the @theme directive in Tailwind v4 to define spacing tokens, you can at least keep your gap values consistent. calc(anchor(bottom) + var(--spacing-2)) where --spacing-2 is 8px from your theme keeps your 8px gap tied to the design system rather than hard-coded. Small thing, but it matters when someone redesigns the spacing scale.
Accessibility: Roles, ARIA, and Keyboard Behavior
The Popover API handles a lot of accessibility automatically. The top-layer popover is focusable and the browser manages focus trapping correctly for dialog role popovers. But for tooltips, you still need to do some work.
For a hint popover that shows on hover or focus, you should use role="tooltip" and wire aria-describedby on the trigger. The popovertarget attribute alone doesn't establish a description relationship. Screen readers won't announce the hint content unless you connect the two explicitly:
<button
popovertarget={id}
aria-describedby={id}
style={{ anchorName }}
>
{label}
</button>
<div
id={id}
role="tooltip"
popover="hint"
style={{ positionAnchor: anchorName }}
>
{hint}
</div>Keyboard behavior is worth testing carefully. The popovertarget attribute makes the button toggle the popover on click. But classic tooltips show on focus, not on click. You can add onFocus and onBlur handlers that call element.showPopover() and element.hidePopover() to get hover/focus semantics. Just make sure you're not fighting the default toggle behavior by also having popovertarget wired up — pick one mechanism and stick with it.
Styling the Popover with the ::backdrop and :popover-open Pseudo-classes
The Popover API ships with two new CSS hooks that are genuinely useful. :popover-open matches the popover only when it's visible, which means you can write entry/exit animations without any JavaScript class toggling. ::backdrop lets you style the inert backdrop that appears behind dialog popovers (less relevant for hints, but essential for modal-style popovers).
Here's how you'd add a fade-in animation to the hint popover. The key is animating from the starting hidden state to the open state, using @starting-style to set the initial values:
.hint-popover {
opacity: 0;
transform: translateY(-4px);
transition:
opacity 120ms ease,
transform 120ms ease,
display 120ms allow-discrete,
overlay 120ms allow-discrete;
}
.hint-popover:popover-open {
opacity: 1;
transform: translateY(0);
}
@starting-style {
.hint-popover:popover-open {
opacity: 0;
transform: translateY(-4px);
}
}The allow-discrete keyword in the transition property is what makes display and overlay animatable. Without it, the popover snaps in and out instantly because display: none isn't transitionable by default. This pattern also works nicely alongside glassmorphic surfaces — if you're building UI with blur-based panels, check out what is glassmorphism for the background patterns that pair well with these floating elements.
One gotcha: @starting-style only works in Chromium 117+ and Safari 17.5+. Firefox support landed in v129. If you need to support older browsers, fall back to a JavaScript-added class on the toggle event.
Integrating with a Theme System
If your app supports dark and light modes — and if you're using something like the theme toggle in React pattern — your popover colors need to respond to the active theme. The good news is that since popover elements live in the top layer, they still inherit CSS custom properties from :root. Your theme tokens flow through correctly.
Define your popover surface colors as CSS variables in your theme layers and you won't need duplicate rules. Something like --popover-bg: rgba(24, 24, 27, 0.95) for dark and rgba(255, 255, 255, 0.95) for light. Then the popover component just uses background: var(--popover-bg) and theme switching works automatically. No conditional class names in the component. No dark: variant gymnastics.
Does this mean you can build a complete tooltip system with zero JavaScript positioning logic? Yes, for Chromium-based browsers in production today. For Firefox and Safari you'll want a progressive enhancement fallback — either Floating UI as a polyfill layer, or accepting that the tooltip just appears in the default browser position without CSS anchor placement. The Tailwind vs CSS Modules comparison is worth reading if you're deciding where to put these anchor positioning rules in a larger project structure.
Browser Support Reality Check and Fallback Strategy
CSS anchor positioning has about 74% global browser support as of late 2026. That sounds decent until you check the breakdown: Firefox 130 shipped it, but only behind layout.css.anchor-positioning.enabled in about:config. Safari 18.2 has it in the stable channel. Chrome has had it since 125. So in practice, you're looking at full support in Chromium, partial in Safari, and basically nothing in Firefox for real users.
The fallback question is: what does the tooltip do when anchor positioning isn't available? The simplest answer is to use @supports to detect the feature and provide a basic fixed-position fallback for browsers that don't support it. The tooltip still appears — just not anchored to the trigger.
/* Fallback for browsers without anchor positioning */
.hint-popover {
position: fixed;
top: 50%;
left: 50%;
translate: -50% -50%;
}
/* Override with anchor positioning where supported */
@supports (anchor-name: --test) {
.hint-popover {
position: absolute;
top: calc(anchor(bottom) + 8px);
left: anchor(center);
translate: -50% 0;
}
}For most developer tools and SaaS dashboards, a Chromium-first strategy is perfectly defensible. If you're building a consumer-facing product where Safari on iOS is significant traffic, hold off on shipping anchor positioning as the primary mechanism. Use it as an enhancement layer while keeping Floating UI as the baseline. The same progressive enhancement mindset applies to other new CSS features — if you're interested in how far the browser rendering pipeline has come, CSS Houdini paint worklets show what's possible when you reach into the paint phase directly.
FAQ
An auto popover closes other open auto popovers when it opens, forming a dismiss stack. A hint popover is lighter — it closes when you interact outside it, but it doesn't dismiss other open popovers. Use hint for tooltips and contextual hints. Use auto for dropdowns, menus, and select panels.
Yes. Anchor positioning is pure CSS and HTML attributes — anchor-name as an inline style and popover as an HTML attribute. Server Components can render the markup. The browser handles all the positioning behavior. No client-side JavaScript is required for basic open/close behavior when you use popovertarget.
The browser needs to resolve anchor positions during layout. If your popover appears in the DOM before the anchor element has painted, you may see a flash at the default position. Make sure the anchor element is rendered before the popover, and use @starting-style for entry animations so the popover starts hidden rather than jumping in from an incorrect position.
@position-try and position-try-fallbacks are part of the same spec as anchor-name, so if a browser supports one it supports the other. As of late 2026, that means Chrome 125+, Edge 125+, and Safari 18.2+. Firefox has the feature behind a flag. Always use @supports (anchor-name: --test) to wrap these rules.
The popovertarget attribute toggles on click. For hover behavior, don't use popovertarget. Instead, add onMouseEnter and onMouseLeave event handlers in React that call popoverElement.showPopover() and popoverElement.hidePopover(). Grab the popover element via a useRef. Also add onFocus and onBlur handlers so keyboard users get the same experience.
Eventually, for most use cases. CSS anchor positioning handles placement, flipping via @position-try, and viewport overflow. It doesn't yet cover arrow elements that point to the trigger, fine-grained offset strategies, or the virtual element concept Floating UI supports. For basic tooltips and simple dropdowns, native anchor positioning is already sufficient in Chromium browsers.