How to build a pure HTML tooltip

The title attribute is the whole answer for most cases. A small block of CSS covers the rest, including keyboard focus. Neither route needs a line of JavaScript.

A pure HTML tooltip is one attribute: put title="..." on any element and the browser draws the tooltip on hover, with no CSS and no script.

<abbr title="Monthly Recurring Revenue">MRR</abbr>
<button title="Copies the table as CSV">Export</button>

That is the complete version. It works in every browser, survives being emailed, and costs nothing. It also gives you no control over position, delay, width or colour, and it does nothing on a touch screen.

A native browser tooltip from a title attribute, hovering over an abbreviation.
A native browser tooltip from a title attribute, hovering over an abbreviation.

This page covers the native version, the CSS-only version for when you need styling, and how to keep either one readable for people who are not using a mouse.

What each route gives you

Route JavaScript Styleable Works on touch Screen reader support
title attribute None No No Partial, varies by reader
CSS pseudo-element None Yes If the trigger is focusable Only with aria-describedby
Popover API None Yes Yes Yes
Script-driven library Yes Yes Yes Depends on the library

Most tables, dashboards and internal reports never need to leave the first two rows.

The CSS-only tooltip

The idea is to store the tooltip text in a data attribute, then draw it with a pseudo-element that is hidden until the trigger is hovered or focused.

<style>
  .tip { position: relative; border-bottom: 1px dotted #888; cursor: help; }
  .tip::after {
    content: attr(data-tip);
    position: absolute;
    left: 50%;
    bottom: 100%;
    transform: translate(-50%, -6px);
    white-space: nowrap;
    background: #1f2430;
    color: #fff;
    padding: 6px 9px;
    border-radius: 6px;
    font-size: 13px;
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s;
  }
  .tip:hover::after,
  .tip:focus-visible::after { opacity: 1; }
</style>

<span class="tip" tabindex="0" data-tip="Excludes cancelled orders">Net units</span>

Four details in that block do the real work, and each one fixes a specific failure.

position: relative on the trigger. Without it the tooltip positions itself against the page rather than the word, and lands somewhere unrelated.

pointer-events: none on the tooltip. Without it the tooltip can sit under the cursor, which hides it again, which shows it again, which flickers.

:focus-visible alongside :hover. This is what makes the tooltip reachable by keyboard, and it is the line most examples on the web leave out.

white-space: nowrap. Keeps a short tip on one line. Drop it and set a max-width instead when the text is a sentence.

A CSS tooltip above a table heading, drawn with a pseudo-element and no script.
A CSS tooltip above a table heading, drawn with a pseudo-element and no script.

Making it work for keyboard and touch

A tooltip that only responds to :hover is invisible to roughly everyone on a phone. Two changes fix most of it.

  1. Make the trigger focusable. A <button> or <a> already is. A <span> is not, so add tabindex="0".
  2. Match :hover with :focus-visible in the CSS, as above.
  3. Describe the trigger properly with aria-describedby when the tip carries information rather than decoration.
<button aria-describedby="tip-net">Net units</button>
<span role="tooltip" id="tip-net" class="sr-tip">Excludes cancelled orders</span>

The rule behind all three is the same. A tooltip is a second reading of something, never the only place a fact appears.

If the reader has to hover to learn the number, put the number on the page. Descriptive labels cover the same ground for controls that have only an icon.

There is one more decision worth making early: what the tip is for. Definitions and unit explanations work well. Instructions do not, because the reader has to keep hovering while trying to act on them.

Column headings in a dense table are the clearest good case. The heading stays short, and the definition sits one hover away for the person who needs it.

Where tooltips get clipped

The most common bug report is not that the tooltip fails to appear, but that it appears cut in half.

That happens when an ancestor has overflow: hidden or overflow: auto, which is normal for scrolling tables and cards. The absolutely positioned tooltip is clipped at that boundary.

Three ways out, in order of effort:

  • Flip the tooltip to the inside, so it points down from the top row rather than up out of the container.
  • Remove overflow from the specific wrapper if nothing depends on it.
  • Use the browser's popover attribute, which renders in the top layer and ignores ancestor clipping.
A tooltip clipped by a scrolling container, with the same tooltip flipped inward beside it.
A tooltip clipped by a scrolling container, with the same tooltip flipped inward beside it.

Test this by opening the finished file in the HTML file opener, scrolling the container, and hovering the first and last rows. Clipping shows up at the edges, not in the middle.

Putting the finished page somewhere people can open it

A tooltip is something you have to move a cursor over, which means a screenshot cannot show it and a PDF cannot carry it. The page has to stay a page.

Paste the HTML into a NOS document and it renders as written, tooltips included, at an address of its own. Share, then Share link, then Create link, and the reader hovers the real thing.

The share dialog. Unlisted by default, and the address stays the same after edits.
The share dialog. Unlisted by default, and the address stays the same after edits.

Because the words in the document stay clickable, correcting the wording of a tip later does not mean regenerating and resending a file. Turning HTML into a link is that step for any page.

Self-contained HTML matters here in particular. A tooltip drawn from an external stylesheet disappears entirely if that stylesheet does not travel with the page, and what the reader sees is a plain word with no hint that anything was meant to happen.

That failure is quiet, which is what makes it worth checking. Nothing errors, nothing logs, and the page looks finished. The only way to catch it is to open the file somewhere that has never seen your folder.

A short checklist

  • Does the tip appear on keyboard focus, not only on hover?
  • Is the trigger a button, a link, or something with tabindex="0"?
  • Is the tooltip text also available somewhere that does not require hovering?
  • Does it survive being scrolled inside its container?
  • Is the CSS inline in the file or otherwise travelling with it?

If all five pass, the tooltip is doing its job without a single line of JavaScript.

Questions people ask

Can you make a tooltip with only HTML?

Yes. Add a title attribute to any element and the browser shows the text on hover. You do not control its position, delay or styling, and it does not appear on touch screens. For those cases you add CSS, but still no JavaScript.

Why does my title tooltip take so long to appear?

The delay before a native tooltip appears is set by the browser and the operating system. There is no HTML or CSS property that changes it. If the delay matters, build the CSS version, where you control the transition yourself.

Do tooltips work on phones?

Native title tooltips do not, because there is no hover on a touch screen. A CSS tooltip that also responds to focus will appear when the element is tapped if it is focusable, such as a button or a link. Never hide information that the reader needs behind hover alone.

How do I make a tooltip accessible to screen readers?

Put the tooltip text in an element with an id, then point at it with aria-describedby from the trigger. Screen readers then read the description after the label. A title attribute is announced by most screen readers too, but support is less consistent.

Keep reading