Pure CSS Tooltips Using attr() and ::after

Pure CSS tooltips allow developers to display contextual hints without relying on JavaScript libraries or extra DOM elements. By combining custom HTML data-* attributes with the CSS attr() function and the ::after pseudo-element, you can dynamically extract text from your markup and render it as a styled tooltip when a user hovers over or focuses on an element. This technique results in clean, performant, and maintainable code that minimizes DOM bloat.

The HTML Structure

The foundation of this technique relies on a custom HTML data attribute, commonly named data-tooltip. You can attach this attribute to any standard HTML element, such as buttons, links, or spans.

<button class="tooltip-trigger" data-tooltip="This is the tooltip text">
  Hover over me
</button>

The attribute value contains the exact string you want to display inside the tooltip.

How the CSS Works

The implementation relies on three main concepts: relative positioning on the parent, dynamic content generation via attr(), and visibility toggling on interaction.

1. Preparing the Parent Container

The parent element must establish a positioning context so that the pseudo-element can be positioned accurately relative to it.

.tooltip-trigger {
  position: relative;
  display: inline-block;
  cursor: pointer;
}

2. Generating the Content with ::after and attr()

The ::after pseudo-element creates an inline-level box inside the element, after its actual content. The CSS content property accepts the attr() functional notation, which retrieves the string stored in the specified HTML attribute.

.tooltip-trigger::after {
  content: attr(data-tooltip);
  position: absolute;
  bottom: 125%;
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 10px;
  border-radius: 4px;
  background-color: #333333;
  color: #ffffff;
  font-size: 0.875rem;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease, transform 0.2s ease;
  z-index: 10;
}

Key properties used above:

  • content: attr(data-tooltip);: Reads the value of data-tooltip directly from the HTML element and injects it into the pseudo-element.
  • pointer-events: none;: Prevents the tooltip from interfering with mouse interactions or flickering when hovered over.
  • opacity: 0; and visibility: hidden;: Keeps the tooltip completely hidden from screen rendering and accessibility flows until triggered.

3. Triggering the Display

To reveal the tooltip, define transition states for both hover and keyboard focus states:

.tooltip-trigger:hover::after,
.tooltip-trigger:focus-visible::after {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(-4px);
}

Using :focus-visible alongside :hover ensures that keyboard users navigating via the Tab key can view the tooltip content just like mouse users.

Optional: Adding a Tooltip Arrow

You can pair the ::after element (the tooltip bubble) with the ::before pseudo-element to create a small pointer arrow underneath the box:

.tooltip-trigger::before {
  content: '';
  position: absolute;
  bottom: 115%;
  left: 50%;
  transform: translateX(-50%);
  border-width: 5px;
  border-style: solid;
  border-color: #333333 transparent transparent transparent;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.2s ease;
}

.tooltip-trigger:hover::before,
.tooltip-trigger:focus-visible::before {
  opacity: 1;
  visibility: visible;
}

Limitations and Considerations

While attr() is efficient for short textual descriptions, it currently supports only string values in stable CSS implementations and cannot interpret HTML markup, links, or images inside the tooltip. For supplementary screen reader support, ensure vital informational text is paired with appropriate aria-label or aria-describedby attributes.