HTML & CSS

CSS3 tooltip: show and hide hint text with a transition

HTML & CSS · · Published 2026-10-26

The title attribute gives you a free tooltip, but you cannot style it, it appears after a delay you do not control, and it never shows for keyboard or touch users. A small amount of HTML and CSS produces a hint that fades in on hover and on focus, sits where you want it and is announced by screen readers.

The markup

The trigger and the tooltip text are both real elements. The trigger points to the text with aria-describedby, so assistive technology reads the hint as a description of the control.

<span class="tip">
  <button type="button" class="tip-trigger" aria-describedby="tip-cvv">What is this?</button>
  <span class="tip-text" role="tooltip" id="tip-cvv">The three digits printed on the back of the card.</span>
</span>

The CSS

.tip {
  position: relative;          /* the tooltip is positioned against this box */
  display: inline-block;
}

.tip-text {
  position: absolute;
  bottom: calc(100% + 10px);   /* above the trigger, with room for the arrow */
  left: 50%;
  width: max-content;
  max-width: 16rem;
  padding: 0.5rem 0.75rem;
  border-radius: 6px;
  background: #222;
  color: #fff;
  font-size: 0.875rem;
  line-height: 1.4;
  text-align: center;

  opacity: 0;
  visibility: hidden;
  transform: translate(-50%, 4px);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0.2s;
  pointer-events: none;
  z-index: 10;
}

/* the arrow */
.tip-text::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 50%;
  margin-left: -6px;
  border: 6px solid transparent;
  border-top-color: #222;
}

/* show on hover and when the trigger has keyboard focus */
.tip:hover .tip-text,
.tip:focus-within .tip-text {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
  transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s linear 0s;
}

@media (prefers-reduced-motion: reduce) {
  .tip-text, .tip:hover .tip-text, .tip:focus-within .tip-text {
    transition-duration: 0.01ms;
    transform: translate(-50%, 0);
  }
}

How each part works

Dismissing with Escape

Accessibility guidance (WCAG 2.1 success criterion 1.4.13, "Content on Hover or Focus") asks that extra content shown on hover or focus can be dismissed without moving the pointer or focus, usually with Escape. A few lines of script add that:

document.addEventListener("keydown", function (e) {
  if (e.key !== "Escape") return;
  document.querySelectorAll(".tip").forEach(function (tip) {
    tip.classList.add("tip-dismissed");
  });
});
document.addEventListener("focusin", function (e) {
  var tip = e.target.closest(".tip");
  if (tip) tip.classList.remove("tip-dismissed");
});
document.addEventListener("mouseover", function (e) {
  var tip = e.target.closest(".tip");
  if (tip) tip.classList.remove("tip-dismissed");
});
.tip.tip-dismissed .tip-text {
  opacity: 0;
  visibility: hidden;
}

The same criterion asks that the tooltip stays visible while the pointer is over it. With pointer-events: none the pointer passes through, which is acceptable for short hints; if your tooltips contain links, remove that rule and add an invisible bridge (padding) between trigger and tooltip so the pointer can travel into it.

Placement variants

/* below the trigger */
.tip--below .tip-text { bottom: auto; top: calc(100% + 10px); }
.tip--below .tip-text::after { top: auto; bottom: 100%; border-top-color: transparent; border-bottom-color: #222; }

/* to the right */
.tip--right .tip-text {
  bottom: auto; left: calc(100% + 10px); top: 50%;
  transform: translate(4px, -50%);
}
.tip--right:hover .tip-text,
.tip--right:focus-within .tip-text { transform: translate(0, -50%); }
.tip--right .tip-text::after {
  top: 50%; left: auto; right: 100%; margin: -6px 0 0;
  border-top-color: transparent; border-right-color: #222;
}

Pure CSS cannot detect that a tooltip would leave the viewport. Near screen edges, choose the variant by hand, or use a small positioning library. Newer CSS anchor positioning will handle this natively once support is broad enough.

With ASP.NET form controls

Server controls render ordinary inputs, so you can wrap them the same way. Point aria-describedby at the hint through the Attributes collection, because TextBox has no property for it:

<span class="tip">
    <asp:Label runat="server" AssociatedControlID="txtCvv" Text="Security code" />
    <asp:TextBox ID="txtCvv" runat="server" MaxLength="4" Columns="4"
        inputmode="numeric" autocomplete="cc-csc" aria-describedby="tip-cvv" />
    <span class="tip-text" role="tooltip" id="tip-cvv">The three or four digits printed on the card.</span>
</span>

Unknown attributes written in the markup of a server control, such as inputmode and aria-describedby here, are passed through to the rendered HTML unchanged. From the code-behind, use txtCvv.Attributes["aria-describedby"] = "tip-cvv";. If the hint is essential to fill the field in correctly, consider showing it permanently under the field instead: a tooltip is for optional help.

When not to use a tooltip

Official documentation