Home / Blog / CSS Tooltips & Popovers: Display Contextual Info on Hover
UI Components 2026-09-23 · 10 min read

CSS Tooltips & Popovers: Display Contextual Info on Hover

Build beautiful CSS tooltips and popovers using pure CSS and the new Popover API. Covers hover, focus, and accessible implementations.

Why CSS Tooltips Still Matter

Tooltips are one of the most frequently needed UI patterns on the web. Whether you need to explain an icon button, clarify a form field, or surface supplementary information without cluttering the layout, a well-crafted tooltip delivers exactly the right information at the right moment. While JavaScript libraries can build tooltips, pure CSS implementations are lighter, faster, and often more than sufficient for most use cases.

In this guide, we'll cover three progressively advanced approaches: a classic CSS-only tooltip using ::before/::after pseudo-elements with position: absolute, a more robust data-tooltip attribute approach, and finally the modern Popover API that HTML now ships natively. We'll also address accessibility throughout — including aria-describedby — because a tooltip that screen reader users can't access is a broken tooltip.

Method 1: Classic CSS Tooltip with Pseudo-Elements

The simplest CSS tooltip uses the ::before tooltip pseudo-element technique. You position a tooltip bubble absolutely relative to the trigger element, then toggle its visibility on :hover and :focus.

The HTML Structure

<button class="tooltip-trigger" aria-describedby="tip1">
  Save File
  <span class="tooltip" role="tooltip" id="tip1">Ctrl + S</span>
</button>

The CSS

.tooltip-trigger {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
}

.tooltip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%);
  background: #1e293b;
  color: #f8fafc;
  font-size: 0.75rem;
  white-space: nowrap;
  padding: 5px 10px;
  border-radius: 6px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease, transform 0.2s ease;
  transform: translateX(-50%) translateY(4px);
}

/* Arrow */
.tooltip::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: #1e293b;
}

/* Show on hover AND focus for keyboard users */
.tooltip-trigger:hover .tooltip,
.tooltip-trigger:focus-within .tooltip {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

Notice two critical details: we use :focus-within so keyboard users who tab to the button also see the tooltip, and we link the tooltip's id to the trigger's aria-describedby so screen readers announce it. The role="tooltip" attribute makes the semantics explicit.

Method 2: The data-tooltip Attribute Pattern

If you have many elements needing tooltips, the data-tooltip attribute pattern is cleaner — you write a single CSS rule set and then simply add data-tooltip="..." to any element.

<!-- HTML: just add the attribute -->
<abbr data-tooltip="Cascading Style Sheets">CSS</abbr>
<button data-tooltip="Delete this item permanently">🗑</button>
/* CSS: one rule to rule them all */
[data-tooltip] {
  position: relative;
  cursor: help;
}

[data-tooltip]::before {
  content: attr(data-tooltip); /* pulls value from HTML attribute */
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translateX(-50%) translateY(4px);
  background: #0f172a;
  color: #e2e8f0;
  font-size: 0.78rem;
  padding: 6px 12px;
  border-radius: 6px;
  white-space: nowrap;
  max-width: 220px;
  white-space: normal;
  text-align: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s ease, transform 0.2s ease;
  z-index: 1000;
}

[data-tooltip]::after {
  content: '';
  position: absolute;
  bottom: calc(100% + 3px);
  left: 50%;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: #0f172a;
  opacity: 0;
  transition: opacity 0.2s ease;
  pointer-events: none;
}

[data-tooltip]:hover::before,
[data-tooltip]:hover::after,
[data-tooltip]:focus::before,
[data-tooltip]:focus::after {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

The magic is content: attr(data-tooltip), which pulls the string directly from the HTML attribute. This keeps your CSS generic and your HTML declarative. You can use our Live HTML/CSS/JS Editor to experiment with variations before adding them to your project.

Tooltip Placement Variants

Tooltips often need to appear on different sides of the trigger. Here's a clean set of placement modifiers:

/* Default: top */
[data-tooltip-pos="bottom"]::before {
  bottom: auto;
  top: calc(100% + 8px);
  transform: translateX(-50%) translateY(-4px);
}
[data-tooltip-pos="bottom"]:hover::before {
  transform: translateX(-50%) translateY(0);
}

[data-tooltip-pos="right"]::before {
  bottom: auto;
  left: calc(100% + 8px);
  top: 50%;
  transform: translateY(-50%) translateX(-4px);
}
[data-tooltip-pos="right"]:hover::before {
  transform: translateY(-50%) translateX(0);
}

[data-tooltip-pos="left"]::before {
  bottom: auto;
  right: calc(100% + 8px);
  left: auto;
  top: 50%;
  transform: translateY(-50%) translateX(4px);
}
[data-tooltip-pos="left"]:hover::before {
  transform: translateY(-50%) translateX(0);
}

Method 3: The Native HTML Popover API

The Popover API is a newer browser-native feature that handles popovers declaratively in HTML — no JavaScript required for basic usage. It manages focus trapping, light-dismiss (click outside to close), and stacking contexts automatically.

Basic Popover Setup

<button popovertarget="my-popover">Show Info</button>

<div id="my-popover" popover>
  <h3>Details</h3>
  <p>This is a popover with richer content than a tooltip.
     It can contain links, buttons, and any HTML.</p>
  <button popovertarget="my-popover" popovertargetaction="hide">Close</button>
</div>
/* Style the popover */
[popover] {
  position: fixed; /* Popovers are in the top layer */
  border: 1px solid #e2e8f0;
  border-radius: 10px;
  padding: 20px;
  max-width: 320px;
  box-shadow: 0 10px 40px rgba(0,0,0,0.15);
  background: #fff;
}

/* Animate with @starting-style and allow-discrete */
[popover] {
  transition:
    opacity 0.2s ease,
    transform 0.2s ease,
    display 0.2s ease allow-discrete,
    overlay 0.2s ease allow-discrete;
  opacity: 1;
  transform: translateY(0);
}

[popover]:not(:popover-open) {
  opacity: 0;
  transform: translateY(8px);
}

@starting-style {
  [popover]:popover-open {
    opacity: 0;
    transform: translateY(8px);
  }
}

Anchor Positioning (CSS Anchor Positioning)

One of the most powerful new features alongside the Popover API is CSS Anchor Positioning, which lets you tether a popover's position directly to its trigger element without JavaScript calculations:

#my-trigger {
  anchor-name: --trigger-anchor;
}

#my-popover {
  position: absolute;
  position-anchor: --trigger-anchor;
  top: anchor(bottom);
  left: anchor(center);
  transform: translateX(-50%);
  margin-top: 8px;
}

This eliminates entire libraries that previously solved the "keep my tooltip pointing at the right element" problem. Browser support is growing rapidly — check caniuse.com for current status.

Accessibility Checklist for Tooltips

  • aria-describedby — Link the tooltip element's ID to the trigger. Screen readers will announce the tooltip text as a description.
  • role="tooltip" — Mark tooltip containers so assistive technologies identify them correctly.
  • Focus visibility — Always show tooltips on :focus or :focus-visible, not just :hover.
  • Keyboard dismissal — Allow Escape key to hide tooltips (requires small JS for rich popovers — the Popover API handles this natively).
  • Sufficient contrast — Tooltip text must meet 4.5:1 contrast ratio against its background.
  • Don't hide essential info — Tooltips should supplement, never replace, visible labels.

Rich Tooltip with a Delay

A common UX improvement is adding a short delay before a tooltip appears to avoid flickering as users move the mouse across the screen:

.tooltip {
  /* ... previous styles ... */
  transition-delay: 0s; /* No delay on hide */
}

.tooltip-trigger:hover .tooltip {
  opacity: 1;
  transition-delay: 0.4s; /* 400ms delay before showing */
}

The delay only applies when showing (the :hover state), not when hiding, which feels natural. Use our CSS Formatter to keep your tooltip stylesheet clean and readable as it grows.

Putting It All Together: Complete Example

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Tooltip Demo</title>
  <style>
    body { font-family: system-ui; display: flex; gap: 20px; padding: 60px; }

    [data-tooltip] {
      position: relative;
      cursor: help;
    }
    [data-tooltip]::before {
      content: attr(data-tooltip);
      position: absolute;
      bottom: calc(100% + 8px);
      left: 50%;
      transform: translateX(-50%) translateY(4px);
      background: #1e293b;
      color: #f1f5f9;
      font-size: 0.78rem;
      padding: 6px 12px;
      border-radius: 6px;
      white-space: nowrap;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.2s ease 0.3s, transform 0.2s ease 0.3s;
      z-index: 100;
    }
    [data-tooltip]:hover::before,
    [data-tooltip]:focus::before {
      opacity: 1;
      transform: translateX(-50%) translateY(0);
      transition-delay: 0s;
    }
  </style>
</head>
<body>
  <button data-tooltip="Copy to clipboard">📋 Copy</button>
  <button data-tooltip="Download as PDF">⬇️ Download</button>
  <abbr data-tooltip="Cascading Style Sheets">CSS</abbr>
</body>
</html>

When to Use Tooltips vs Popovers

The distinction is important for both UX and accessibility:

  • Tooltip — Short, single-line text. Appears on hover/focus. Non-interactive. Use for icon labels, keyboard shortcuts, brief explanations.
  • Popover — Richer content — HTML, buttons, links. User-triggered. Can be dismissed. Use for settings panels, contextual help, date pickers, dropdown menus.

The native Popover API is specifically designed for the popover use case, not for simple text tooltips. Keep CSS-only tooltips for simple labels and reserve the Popover API for interactive panels.

Frequently Asked Questions

Can I build a CSS tooltip without JavaScript?
Yes. Using pseudo-elements (::before/::after) with position: absolute and toggling opacity on :hover and :focus-within, you can build fully functional tooltips with zero JavaScript. The data-tooltip attribute pattern makes this even easier to scale across many elements.
What is the difference between a tooltip and a popover?
Tooltips display short, non-interactive text on hover or focus. Popovers are richer UI panels that can contain HTML, buttons, and links — they're triggered by click and can be dismissed. The native HTML Popover API handles popovers with built-in focus management and light-dismiss behavior.
How do I make a CSS tooltip accessible to screen readers?
Add role="tooltip" to the tooltip element, give it a unique id, and link it to the trigger via aria-describedby="tooltip-id". Also ensure the tooltip appears on :focus, not only on :hover, so keyboard users can access it.
What is the Popover API in HTML?
The Popover API is a native browser feature (available in modern browsers) that lets you create accessible popovers declaratively using the popover attribute and popovertarget on a button. It handles stacking contexts, light-dismiss, focus management, and keyboard (Escape) dismissal automatically — no JavaScript required for basic usage.
How do I add an arrow to a CSS tooltip?
Use the ::after pseudo-element on the tooltip bubble. Set its content to an empty string, position it at the edge of the bubble facing the trigger, and use the CSS border trick — set all borders transparent except the one facing the trigger element — to create a triangle arrow shape.
What is CSS Anchor Positioning?
CSS Anchor Positioning is a new CSS feature (anchor-name / position-anchor) that lets you declaratively tether a positioned element (like a tooltip or popover) to another element (its anchor) so the two stay aligned without JavaScript position calculations. It's designed to work hand-in-hand with the Popover API.

Try Our Free HTML CSS Tools

Practice what you've learned with our free online tools — no installation required.

Live HTML/CSS Editor HTML Formatter CSS Formatter