CSSTooltip Patterns

Tooltip Patterns

A tooltip is a small piece of supplementary text that appears near an element on hover or focus — a term definition, an abbreviation expansion, a hint about what an icon button does. The interesting CSS problems are positioning it without clipping, delaying its appearance so it does not flash on every incidental mouse pass, and making sure keyboard and screen reader users get the same information mouse users do.

Pure CSS tooltip with pseudo-elements and a data attribute

The classic no-JavaScript tooltip: store the tooltip text in a data attribute, then pull it into a generated ::before/::after box shown only on hover/focus.

HTML
<button class="has-tooltip" data-tooltip="Copy to clipboard" aria-describedby="tt-copy">
  Copy
</button>

CSS
.has-tooltip {
  position: relative;
}

.has-tooltip::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: calc(100% + 6px);
  left: 50%;
  transform: translateX(-50%);
  padding: 4px 8px;
  border-radius: 4px;
  background: #1e293b;
  color: white;
  font-size: 0.8rem;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s ease;
}

.has-tooltip:hover::after,
.has-tooltip:focus-visible::after {
  opacity: 1;
}
  • content: attr(data-tooltip) pulls the label straight from the markup — one source of truth, no duplicated text in the CSS.

  • pointer-events: none on the tooltip box keeps it from intercepting hover/click events meant for whatever is underneath it once it fades.

  • Triggering on both :hover and :focus-visible (not just :hover) is what makes this keyboard-accessible at all — see the accessibility section below.

Positioning quirks

A tooltip positioned with plain absolute positioning relative to its trigger can silently clip against a scrolling or overflow: hidden ancestor, or run off the edge of the viewport near screen edges — the two most common tooltip bugs in production.

Problem

Cause

Fix

Tooltip gets clipped/cut off

An ancestor has overflow: hidden/auto/scroll

Portal the tooltip to document.body (JS), or use the Anchor Positioning API below, which is not constrained by ancestor overflow

Tooltip runs off-screen near an edge

Fixed placement (always "above, centered") does not adapt to available space

Use @position-try fallbacks with anchor positioning, or JS-based collision detection

Tooltip appears behind other content

A low or default z-index, or trapped in a low stacking context

Raise z-index deliberately, or again, prefer the top-layer behavior of popover

The modern fix — CSS Anchor Positioning

The anchor positioning API lets an element position itself relative to another element anywhere in the DOM — not just an ancestor — solving the overflow-clipping problem at the source.

CSS
.trigger {
  anchor-name: --copy-btn;
}

.tooltip {
  position: fixed;
  position-anchor: --copy-btn;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% -6px;
}
Note
See the dedicated Anchor Positioning page for the full API, including @position-try fallback positions that automatically flip the tooltip when it would otherwise overflow the viewport — the declarative replacement for hand-rolled JS collision detection.
The popover attribute approach

For a tooltip that should render above absolutely everything (including inside a scroll container or a modal), the popover attribute (see Modal & Dialog Patterns) renders in the top layer, sidestepping z-index and overflow clipping entirely.

HTML
<button popovertarget="tt-copy" popovertargetaction="show">Copy</button>
<div id="tt-copy" popover="hint" role="tooltip">Copy to clipboard</div>
Note
popover="hint" is a lighter-weight popover mode built specifically for this case: unlike popover="auto", a hint popover does not close other open auto popovers when it opens, which matters since a tooltip should be able to appear over an open menu without dismissing it.
Delays with transition

A tooltip that appears the instant the cursor grazes an element is noisy. Delay the entrance (not the exit) with transition-delay.

CSS
.has-tooltip::after {
  opacity: 0;
  transition: opacity 0.15s ease 0s; /* no delay hiding */
}

.has-tooltip:hover::after {
  transition-delay: 0.4s; /* wait before showing */
}
Tip
Only delay the show, never the hide — an instant hide keeps the UI feeling responsive when the user moves away, while the show delay is what filters out incidental mouse passes.
Accessibility — aria-describedby, hover, and focus
  • A tooltip must be reachable by keyboard, not just mouse — trigger it on :focus-visible in addition to :hover, never :hover alone.

  • Associate the tooltip text with its trigger via aria-describedby="id" pointing at the tooltip element's id, so screen readers announce it as supplementary description, not just visually-adjacent text.

  • Give the tooltip element role="tooltip" so assistive tech understands its purpose, especially when using the popover or anchor-positioning approaches rather than a pure ::after (which has no separate DOM node to attach a role to).

  • Never put essential, non-duplicated information only inside a tooltip — touch-only users have no hover state at all, so critical info needs a fallback (a visible label, an always-present icon caption).

Warning
Do not rely on title attribute tooltips for anything important — they are inconsistent across browsers, invisible on touch devices, and cannot be styled at all. Treat title as a last-resort fallback, never the primary implementation.