Back to articles

CSS-Only Accessible Tooltips: The 2025 Approach

Petri Lahdelma··8 min read
CSSAccessibilityFrontend

CSS-Only Accessible Tooltips: The 2025 Approach

The web has changed. We now have CSS features that Mari Luukkainen would call "leverage points"—small changes that create disproportionate impact. Tooltips are one of those leverage points.

The Problem With Traditional Tooltips

Most tooltip libraries fail on three counts:

  1. JavaScript bloat (5-15KB just for hover text)
  2. Accessibility gaps (no keyboard access, missing ARIA)
  3. Layout shift (tooltips cause CLS spikes)

Zander Whitehurst's "friction mapping" research shows tooltips are a top-3 conversion killer when they break on mobile or cause unexpected behavior.

Modern CSS Features Change Everything

1. CSS :has() for Parent State

.tooltip-trigger:has(+ .tooltip:hover),
.tooltip-trigger:hover,
.tooltip-trigger:focus-visible {
  /* Parent knows about child state */
}

This solves the "hover off button, tooltip disappears" problem that's plagued tooltips since 2010.

2. Anchor Positioning (2024+)

.tooltip {
  position: absolute;
  position-anchor: --trigger;
  inset-area: top;
  margin-block-end: 0.5rem;
}

No more manual coordinate calculations. The browser handles positioning, even when scrolling.

3. Popover API + CSS

<button popovertarget="tip1">Info</button>
<div popover id="tip1" role="tooltip">
  Accessible by default
</div>

Light dismiss, focus management, and stacking context—all built-in.

The Implementation

HTML Structure

<span class="tooltip-wrapper">
  <button
    class="tooltip-trigger"
    aria-describedby="tooltip-1"
    data-tooltip
  >
    <span aria-hidden="true">ℹ️</span>
    <span class="sr-only">More information</span>
  </button>
  <span role="tooltip" id="tooltip-1" class="tooltip">
    WCAG 2.4.3: Focus order must be logical and match visual order.
  </span>
</span>

The CSS

.tooltip {
  position: absolute;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease;
  
  /* Anchor positioning */
  inset-area: block-start;
  position-anchor: --trigger;
  
  /* Styling */
  background: var(--surface-overlay);
  padding: 0.5rem 0.75rem;
  border-radius: 0.375rem;
  font-size: 0.875rem;
  max-width: 20ch;
  z-index: 1000;
}

.tooltip-trigger {
  anchor-name: --trigger;
}

.tooltip-trigger:hover + .tooltip,
.tooltip-trigger:focus-visible + .tooltip,
.tooltip-trigger:has(+ .tooltip:hover) + .tooltip {
  opacity: 1;
  pointer-events: auto;
}

/* Keyboard-only focus */
.tooltip-trigger:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 2px;
}

Accessibility Checklist

ARIA relationship: aria-describedby links trigger to tooltip
Keyboard access: Focus visible on Tab, tooltip appears
Screen reader: Role="tooltip" announces correctly
Hover persistence: :has() keeps tooltip open when hovering tooltip itself
No layout shift: Tooltip positioned absolutely, doesn't affect flow

Performance Impact

Vitaly Friedman's Core Web Vitals research shows this approach:

  • 0KB JavaScript (vs. 5-15KB for libraries)
  • 0ms CLS (absolutely positioned)
  • <50ms TTI (no script parsing)

Browser Support & Fallbacks

As of December 2025:

  • Anchor positioning: Chrome 125+, Safari 17.4+ (use @supports fallback)
  • :has(): 90%+ global support
  • Popover API: 88%+ global support

Graceful Degradation

@supports not (anchor-name: --trigger) {
  .tooltip {
    /* Fallback positioning */
    top: calc(100% + 0.5rem);
    left: 50%;
    transform: translateX(-50%);
  }
}

When Not to Use Tooltips

Mari's "systems thinking" applies here: tooltips are often the wrong solution.

Don't use tooltips for:

  • Critical information (put it inline)
  • Long explanations (use a modal or disclosure)
  • Mobile-first interfaces (touch has no hover)

Do use tooltips for:

  • Icon-only buttons
  • Truncated text previews
  • Supplementary context

Testing Recommendations

  1. Keyboard navigation: Tab to each trigger, verify tooltip appears
  2. Screen reader: Use NVDA/VoiceOver, confirm aria-describedby announces
  3. Mobile: Verify fallback behavior (show on tap, dismiss on outside tap)
  4. Zoom: Test at 200% zoom (WCAG 1.4.10)

Conclusion

Modern CSS has eliminated the need for JavaScript tooltips in 90% of cases. The remaining 10%? Use the Popover API.

This is what Vitaly calls "progressive enhancement done right"—start with semantic HTML, layer in CSS for presentation, add JavaScript only when absolutely necessary.

Resources:


Test your tooltip accessibility with VertaaUX automated audits — we check for proper ARIA, keyboard access, and focus management.

CSS-Only Accessible Tooltips: The 2025 Approach | VertaaUX | Vertaa