Tailwind Tooltip: 16 Examples of Tooltips and Popovers

Tailwind CSS doesn't include a tooltip, so you put one together from utilities: a relative wrapper marked group and an absolutely positioned child that group-hover switches on. In Tailwind CSS v4, that hover-only version never opens on touch screens.

Below are 16 Tailwind tooltip examples you can copy, followed by a guide to placement, arrows, touch, keyboard access and WCAG 1.4.13.

By Bogdan Sandu ยท Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Placement and hover-plus-focus tooltips

Pure-utility tooltips: a relative wrapper marked group, an absolutely positioned span with role="tooltip", and group-hover plus group-focus-within to reveal it. No JavaScript in this group.

Four placements with arrows

  • group-hover:
  • group-focus-within:
  • bottom-full
  • rotate-45

Hover a control or press Tab to reach it with the keyboard.

Each side is two moves: push the tooltip off one edge with bottom-full, top-full, right-full or left-full, then center it on the other axis with a 1/2 offset and a translate. Padding instead of margin keeps the gap inside the group so the pointer can cross it, and the arrow is a rotated size-2 square in the same color. The buttons stack on phones so the side tooltips have room.

View code
<div class="w-full max-w-xl rounded-2xl bg-white px-6 py-12 shadow-sm ring-1 ring-slate-900/5">
  <div class="grid grid-cols-1 justify-items-center gap-y-14 md:grid-cols-3 md:grid-rows-3 md:gap-x-10 md:gap-y-6">
    <div class="group relative inline-flex md:col-start-2 md:row-start-1">
      <button type="button" aria-describedby="ttp-pl-top" class="min-h-11 rounded-lg bg-slate-100 px-4 text-sm font-semibold text-slate-800 hover:bg-slate-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">Top</button>
      <span id="ttp-pl-top" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none">
        <span class="relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 font-mono text-xs text-white">bottom-full<span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span>
      </span>
    </div>
    <div class="group relative inline-flex md:col-start-1 md:row-start-2">
      <button type="button" aria-describedby="ttp-pl-left" class="min-h-11 rounded-lg bg-slate-100 px-4 text-sm font-semibold text-slate-800 hover:bg-slate-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">Left</button>
      <span id="ttp-pl-left" role="tooltip" class="invisible absolute right-full top-1/2 z-10 -translate-y-1/2 pr-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none">
        <span class="relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 font-mono text-xs text-white">right-full<span aria-hidden="true" class="absolute left-full top-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span>
      </span>
    </div>
    <div class="hidden place-items-center md:col-start-2 md:row-start-2 md:grid">
      <span class="grid size-14 place-items-center rounded-full bg-sky-50 text-sky-700 ring-1 ring-sky-200" aria-hidden="true">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" class="size-6"><path d="M12 3v18M3 12h18M12 3l-2.5 2.5M12 3l2.5 2.5M12 21l-2.5-2.5M12 21l2.5-2.5M3 12l2.5-2.5M3 12l2.5 2.5M21 12l-2.5-2.5M21 12l-2.5 2.5"/></svg>
      </span>
    </div>
    <div class="group relative inline-flex md:col-start-3 md:row-start-2">
      <button type="button" aria-describedby="ttp-pl-right" class="min-h-11 rounded-lg bg-slate-100 px-4 text-sm font-semibold text-slate-800 hover:bg-slate-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">Right</button>
      <span id="ttp-pl-right" role="tooltip" class="invisible absolute left-full top-1/2 z-10 -translate-y-1/2 pl-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none">
        <span class="relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 font-mono text-xs text-white">left-full<span aria-hidden="true" class="absolute right-full top-1/2 size-2 translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span>
      </span>
    </div>
    <div class="group relative inline-flex md:col-start-2 md:row-start-3">
      <button type="button" aria-describedby="ttp-pl-bottom" class="min-h-11 rounded-lg bg-slate-100 px-4 text-sm font-semibold text-slate-800 hover:bg-slate-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">Bottom</button>
      <span id="ttp-pl-bottom" role="tooltip" class="invisible absolute left-1/2 top-full z-10 -translate-x-1/2 pt-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none">
        <span class="relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 font-mono text-xs text-white">top-full<span aria-hidden="true" class="absolute bottom-full left-1/2 size-2 -translate-x-1/2 translate-y-1/2 rotate-45 bg-slate-900"></span></span>
      </span>
    </div>
  </div>
</div>

Icon toolbar labelled by its tooltips

  • role="toolbar"
  • aria-labelledby
  • group-focus-within:
  • size-11

Hover a control or press Tab to reach it with the keyboard.

Every icon button points aria-labelledby at its own tooltip, so the visible label and the accessible name can never drift apart, and the reference still works while the tooltip is invisible. Bottom placement keeps the labels clear of the text below the toolbar. The 44px buttons come from size-11 and the active state from aria-pressed:bg-amber-100.

View code
<div class="w-full max-w-md overflow-visible rounded-xl border border-stone-200 bg-white shadow-sm">
  <div role="toolbar" aria-label="Text formatting" class="flex flex-wrap items-center gap-1 border-b border-stone-200 bg-stone-50 p-1.5 rounded-t-xl">
    <div class="group relative">
      <button type="button" aria-pressed="true" aria-labelledby="ttp-tb-bold" class="grid size-11 place-items-center rounded-lg text-stone-700 hover:bg-stone-200 aria-pressed:bg-amber-100 aria-pressed:text-amber-900 focus-visible:outline-2 focus-visible:outline-amber-600"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/></svg></button>
      <span id="ttp-tb-bold" role="tooltip" class="invisible absolute left-1/2 top-full z-10 -translate-x-1/2 pt-1.5 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-stone-900 px-2 py-1 text-xs font-medium text-white">Bold</span></span>
    </div>
    <div class="group relative">
      <button type="button" aria-pressed="false" aria-labelledby="ttp-tb-italic" class="grid size-11 place-items-center rounded-lg text-stone-700 hover:bg-stone-200 aria-pressed:bg-amber-100 aria-pressed:text-amber-900 focus-visible:outline-2 focus-visible:outline-amber-600"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M10 5h8M6 19h8M14 5l-4 14"/></svg></button>
      <span id="ttp-tb-italic" role="tooltip" class="invisible absolute left-1/2 top-full z-10 -translate-x-1/2 pt-1.5 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-stone-900 px-2 py-1 text-xs font-medium text-white">Italic</span></span>
    </div>
    <div class="group relative">
      <button type="button" aria-labelledby="ttp-tb-link" class="grid size-11 place-items-center rounded-lg text-stone-700 hover:bg-stone-200 focus-visible:outline-2 focus-visible:outline-amber-600"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></svg></button>
      <span id="ttp-tb-link" role="tooltip" class="invisible absolute left-1/2 top-full z-10 -translate-x-1/2 pt-1.5 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-stone-900 px-2 py-1 text-xs font-medium text-white">Insert link</span></span>
    </div>
    <span aria-hidden="true" class="mx-1 h-6 w-px bg-stone-300"></span>
    <div class="group relative">
      <button type="button" aria-labelledby="ttp-tb-list" class="grid size-11 place-items-center rounded-lg text-stone-700 hover:bg-stone-200 focus-visible:outline-2 focus-visible:outline-amber-600"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/></svg></button>
      <span id="ttp-tb-list" role="tooltip" class="invisible absolute left-1/2 top-full z-10 -translate-x-1/2 pt-1.5 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-stone-900 px-2 py-1 text-xs font-medium text-white">Bulleted list</span></span>
    </div>
    <div class="group relative">
      <button type="button" aria-labelledby="ttp-tb-quote" class="grid size-11 place-items-center rounded-lg text-stone-700 hover:bg-stone-200 focus-visible:outline-2 focus-visible:outline-amber-600"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M6 6v12M10 8h9M10 12h9M10 16h6"/></svg></button>
      <span id="ttp-tb-quote" role="tooltip" class="invisible absolute left-1/2 top-full z-10 -translate-x-1/2 pt-1.5 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-stone-900 px-2 py-1 text-xs font-medium text-white">Quote</span></span>
    </div>
  </div>
  <div class="p-4">
    <p class="text-base font-semibold text-stone-900">Harbor Street spring menu</p>
    <p class="mt-1 text-sm leading-6 text-stone-600">Draft notes for the kitchen team: swap the winter squash soup for a chilled pea broth and move the tasting to Thursday.</p>
  </div>
</div>

Sidebar rail with delayed, sliding labels

  • group-hover:delay-300
  • transition-[opacity,visibility,translate]
  • motion-reduce:
  • -translate-x-1

Hover a control or press Tab to reach it with the keyboard.

The labels wait 300 ms before showing, so a pointer sweeping down the rail does not flash every label, while the hide stays instant because only the hover and focus states carry the delay. In v4 translate is its own property, so the transition list names translate instead of transform. motion-reduce:transition-none and motion-reduce:translate-x-0 drop the slide for people who ask for less motion.

View code
<div class="flex w-full max-w-md overflow-visible rounded-2xl bg-white shadow-sm ring-1 ring-zinc-900/5">
  <nav aria-label="Workspace" class="flex flex-col items-center gap-2 rounded-l-2xl bg-zinc-900 px-2 py-4">
    <div class="group relative">
      <a href="#" aria-current="page" aria-label="Inbox" aria-describedby="ttp-rl-inbox" class="grid size-11 place-items-center rounded-xl bg-lime-300 text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-300"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5"><path d="M3 13h5l1.5 3h5L16 13h5M5 5h14l2 8v6H3v-6z"/></svg></a>
      <span id="ttp-rl-inbox" role="tooltip" class="invisible absolute left-full top-1/2 z-10 -translate-x-1 -translate-y-1/2 pl-3 opacity-0 transition-[opacity,visibility,translate] duration-200 group-hover:visible group-hover:translate-x-0 group-hover:opacity-100 group-hover:delay-300 group-focus-within:visible group-focus-within:translate-x-0 group-focus-within:opacity-100 group-focus-within:delay-300 motion-reduce:translate-x-0 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-lg bg-zinc-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-lg">Inbox <span class="text-lime-300">12 new</span></span></span>
    </div>
    <div class="group relative">
      <a href="#" aria-label="Calendar" aria-describedby="ttp-rl-cal" class="grid size-11 place-items-center rounded-xl text-zinc-300 hover:bg-zinc-800 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-300"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5"><path d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4"/></svg></a>
      <span id="ttp-rl-cal" role="tooltip" class="invisible absolute left-full top-1/2 z-10 -translate-x-1 -translate-y-1/2 pl-3 opacity-0 transition-[opacity,visibility,translate] duration-200 group-hover:visible group-hover:translate-x-0 group-hover:opacity-100 group-hover:delay-300 group-focus-within:visible group-focus-within:translate-x-0 group-focus-within:opacity-100 group-focus-within:delay-300 motion-reduce:translate-x-0 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-lg bg-zinc-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-lg">Calendar</span></span>
    </div>
    <div class="group relative">
      <a href="#" aria-label="Files" aria-describedby="ttp-rl-files" class="grid size-11 place-items-center rounded-xl text-zinc-300 hover:bg-zinc-800 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-300"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5"><path d="M3 6h7l2 2h9v11H3z"/></svg></a>
      <span id="ttp-rl-files" role="tooltip" class="invisible absolute left-full top-1/2 z-10 -translate-x-1 -translate-y-1/2 pl-3 opacity-0 transition-[opacity,visibility,translate] duration-200 group-hover:visible group-hover:translate-x-0 group-hover:opacity-100 group-hover:delay-300 group-focus-within:visible group-focus-within:translate-x-0 group-focus-within:opacity-100 group-focus-within:delay-300 motion-reduce:translate-x-0 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-lg bg-zinc-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-lg">Shared files</span></span>
    </div>
    <div class="group relative">
      <a href="#" aria-label="Settings" aria-describedby="ttp-rl-set" class="grid size-11 place-items-center rounded-xl text-zinc-300 hover:bg-zinc-800 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-300"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" class="size-5"><path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19 12l2-1-1-3-2.2.2-1.3-1.3L16.7 5l-3-1-1 2h-1.4l-1-2-3 1 .2 2.2L6.2 8.5 4 8.3l-1 3 2 1v1.4l-2 1 1 3 2.2-.2 1.3 1.3-.2 2.2 3 1 1-2h1.4l1 2 3-1-.2-2.2 1.3-1.3 2.2.2 1-3-2-1z"/></svg></a>
      <span id="ttp-rl-set" role="tooltip" class="invisible absolute left-full top-1/2 z-10 -translate-x-1 -translate-y-1/2 pl-3 opacity-0 transition-[opacity,visibility,translate] duration-200 group-hover:visible group-hover:translate-x-0 group-hover:opacity-100 group-hover:delay-300 group-focus-within:visible group-focus-within:translate-x-0 group-focus-within:opacity-100 group-focus-within:delay-300 motion-reduce:translate-x-0 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-lg bg-zinc-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-lg">Settings</span></span>
    </div>
  </nav>
  <div class="min-w-0 flex-1 p-5">
    <p class="text-sm font-semibold text-zinc-900">Inbox</p>
    <ul class="mt-3 space-y-3 text-sm">
      <li class="rounded-lg bg-zinc-50 p-3"><p class="font-medium text-zinc-900">Mara Okafor</p><p class="truncate text-zinc-600">Venue contract for the June retreat</p></li>
      <li class="rounded-lg bg-zinc-50 p-3"><p class="font-medium text-zinc-900">Tide &amp; Pine Studio</p><p class="truncate text-zinc-600">Logo round two is ready for review</p></li>
      <li class="rounded-lg bg-zinc-50 p-3"><p class="font-medium text-zinc-900">Billing</p><p class="truncate text-zinc-600">Invoice 2041 was paid</p></li>
    </ul>
  </div>
</div>

Avatar stack with named groups

  • group/card
  • group/avatar
  • group-hover/avatar:
  • -space-x-2

Hover a control or press Tab to reach it with the keyboard.

The card is group/card and lifts on hover, while each avatar is group/avatar, so group-hover/avatar:visible opens only the tooltip of the avatar under the pointer instead of every tooltip in the card. Without named groups the outer hover would reveal all of them at once. Avatars are initials on colored circles, with ring-2 ring-white separating the overlap.

View code
<article class="group/card w-full max-w-sm rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5 transition-shadow hover:shadow-md motion-reduce:transition-none">
  <div class="flex items-start justify-between gap-3">
    <div>
      <h3 class="text-base font-semibold text-slate-900">Checkout redesign</h3>
      <p class="mt-1 text-sm text-slate-600">Sprint 14, due Friday</p>
    </div>
    <span class="rounded-full bg-violet-100 px-2.5 py-1 text-xs font-semibold text-violet-800">In review</span>
  </div>
  <div class="mt-8 flex items-center justify-between">
    <ul class="flex -space-x-2" aria-label="Team">
      <li class="group/avatar relative">
        <button type="button" aria-label="Priya Nair" aria-describedby="ttp-av-1" class="grid size-10 place-items-center rounded-full bg-violet-600 text-sm font-semibold text-white ring-2 ring-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600">PN</button>
        <span id="ttp-av-1" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover/avatar:visible group-hover/avatar:opacity-100 group-focus-within/avatar:visible group-focus-within/avatar:opacity-100 motion-reduce:transition-none"><span class="relative block whitespace-nowrap rounded-lg bg-slate-900 px-2.5 py-1.5 text-xs text-white"><span class="font-semibold">Priya Nair</span> <span class="text-slate-300">Design lead</span><span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span></span>
      </li>
      <li class="group/avatar relative">
        <button type="button" aria-label="Tomรกs Ruiz" aria-describedby="ttp-av-2" class="grid size-10 place-items-center rounded-full bg-teal-700 text-sm font-semibold text-white ring-2 ring-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600">TR</button>
        <span id="ttp-av-2" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover/avatar:visible group-hover/avatar:opacity-100 group-focus-within/avatar:visible group-focus-within/avatar:opacity-100 motion-reduce:transition-none"><span class="relative block whitespace-nowrap rounded-lg bg-slate-900 px-2.5 py-1.5 text-xs text-white"><span class="font-semibold">Tomรกs Ruiz</span> <span class="text-slate-300">Frontend</span><span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span></span>
      </li>
      <li class="group/avatar relative">
        <button type="button" aria-label="Ada Lindqvist" aria-describedby="ttp-av-3" class="grid size-10 place-items-center rounded-full bg-orange-700 text-sm font-semibold text-white ring-2 ring-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600">AL</button>
        <span id="ttp-av-3" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover/avatar:visible group-hover/avatar:opacity-100 group-focus-within/avatar:visible group-focus-within/avatar:opacity-100 motion-reduce:transition-none"><span class="relative block whitespace-nowrap rounded-lg bg-slate-900 px-2.5 py-1.5 text-xs text-white"><span class="font-semibold">Ada Lindqvist</span> <span class="text-slate-300">QA</span><span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span></span>
      </li>
      <li class="group/avatar relative">
        <button type="button" aria-label="3 more people" aria-describedby="ttp-av-more" class="grid size-10 place-items-center rounded-full bg-slate-200 text-xs font-semibold text-slate-700 ring-2 ring-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-600">+3</button>
        <span id="ttp-av-more" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover/avatar:visible group-hover/avatar:opacity-100 group-focus-within/avatar:visible group-focus-within/avatar:opacity-100 motion-reduce:transition-none"><span class="relative block w-max rounded-lg bg-slate-900 px-3 py-2 text-xs leading-5 text-white">Jonah Pike<br>Leila Haddad<br>Sven Morel<span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span></span>
      </li>
    </ul>
    <p class="text-sm font-medium text-slate-700">18 of 24 tasks</p>
  </div>
</article>

Dismissible, disabled and changing tooltips

The extra work WCAG 1.4.13 and real interfaces ask for: Escape to dismiss, a wrapper for a disabled button, a Copied! state and tooltips that only appear when text is cut off.

Map controls with Escape to dismiss

  • hidden attribute
  • group-focus-within:
  • pr-2
  • keydown

Tab to a control, then press Escape: the tooltip closes and focus stays put.

WCAG 1.4.13 wants hover and focus content to be dismissible without moving focus. A keydown listener sets the native hidden attribute on every tooltip when Escape is pressed, and Tailwind v4 preflight makes [hidden] win over display utilities. Moving the pointer back in or refocusing a control removes the attribute, and the pr-2 bridge keeps each tooltip hoverable.

View code
<figure class="relative w-full max-w-lg overflow-visible rounded-2xl bg-slate-900 shadow-lg">
  <img src="coast-road.webp" width="800" height="600" alt="Pine-covered cliffs above a calm sea, with a narrow road winding along the coastline at sunset" class="aspect-[4/3] w-full rounded-2xl object-cover">
  <figcaption class="absolute bottom-3 left-3 rounded-lg bg-white/90 px-3 py-1.5 text-xs font-semibold text-slate-900">Coast road, 38 km</figcaption>
  <div class="absolute right-3 top-3 flex flex-col gap-2">
    <div class="ttp-esc group relative">
      <button type="button" aria-label="Zoom in" aria-describedby="ttp-esc-1" class="grid size-11 place-items-center rounded-xl bg-white text-slate-800 shadow hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M12 5v14M5 12h14"/></svg></button>
      <span id="ttp-esc-1" role="tooltip" class="ttp-esc-tip invisible absolute right-full top-1/2 z-10 -translate-y-1/2 pr-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white">Zoom in</span></span>
    </div>
    <div class="ttp-esc group relative">
      <button type="button" aria-label="Zoom out" aria-describedby="ttp-esc-2" class="grid size-11 place-items-center rounded-xl bg-white text-slate-800 shadow hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M5 12h14"/></svg></button>
      <span id="ttp-esc-2" role="tooltip" class="ttp-esc-tip invisible absolute right-full top-1/2 z-10 -translate-y-1/2 pr-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white">Zoom out</span></span>
    </div>
    <div class="ttp-esc group relative">
      <button type="button" aria-label="Center on my location" aria-describedby="ttp-esc-3" class="grid size-11 place-items-center rounded-xl bg-white text-slate-800 shadow hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 2v3M12 19v3M2 12h3M19 12h3"/></svg></button>
      <span id="ttp-esc-3" role="tooltip" class="ttp-esc-tip invisible absolute right-full top-1/2 z-10 -translate-y-1/2 pr-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white">Center on my location</span></span>
    </div>
  </div>
</figure>

Tooltips on disabled buttons

  • disabled:
  • aria-disabled:
  • tabindex="0"
  • cursor-not-allowed

Hover or focus a control. Press Escape where the demo says so.

A disabled button fires no pointer events and cannot take focus, so the first tooltip hangs off a focusable span wrapper that carries group and aria-describedby. The second button uses aria-disabled="true" instead: it stays focusable, aria-disabled: utilities dim it, and a click handler refuses the action. Both explain why the action is unavailable instead of leaving people guessing.

View code
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center justify-between border-b border-slate-100 pb-4">
    <p class="text-sm text-slate-600">Order total</p>
    <p class="text-xl font-semibold text-slate-900">$184.20</p>
  </div>
  <p class="mt-4 rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900">No shipping address on file yet.</p>
  <div class="mt-14 flex flex-wrap gap-3">
    <span tabindex="0" aria-describedby="ttp-dis-1" class="group relative inline-flex rounded-lg focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700">
      <button type="button" disabled class="min-h-11 rounded-lg bg-emerald-700 px-5 text-sm font-semibold text-white disabled:pointer-events-none disabled:bg-slate-200 disabled:text-slate-500">Place order</button>
      <span id="ttp-dis-1" role="tooltip" class="invisible absolute bottom-full left-0 z-10 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="relative block w-max max-w-56 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs leading-5 text-white">Add a shipping address to place the order<span aria-hidden="true" class="absolute left-6 top-full size-2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span></span>
    </span>
    <span class="group relative inline-flex">
      <button type="button" aria-disabled="true" aria-describedby="ttp-dis-2" class="ttp-soft-disabled min-h-11 rounded-lg border border-slate-300 bg-white px-5 text-sm font-semibold text-slate-800 hover:bg-slate-50 aria-disabled:cursor-not-allowed aria-disabled:border-slate-200 aria-disabled:text-slate-500 aria-disabled:hover:bg-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700">Save as quote</button>
      <span id="ttp-dis-2" role="tooltip" class="invisible absolute bottom-full right-0 z-10 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="relative block w-max max-w-52 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs leading-5 text-white">Quotes need a company account<span aria-hidden="true" class="absolute right-6 top-full size-2 -translate-y-1/2 rotate-45 bg-slate-900"></span></span></span>
    </span>
  </div>
</div>

Copy button with a Copied! state

  • group-data-copied:
  • data-copied
  • role="status"
  • setTimeout

Click the copy button.

A click sets data-copied on the wrapper: group-data-copied: swaps the clipboard icon for a check, turns the tooltip green and keeps it visible, and the script changes the tooltip text to Copied!. An sr-only live region announces the result. The demo only changes state; a real page would call navigator.clipboard.writeText before showing the confirmation.

View code
<div class="w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5">
  <label for="ttp-invite" class="text-sm font-semibold text-slate-900">Invite link</label>
  <p class="mt-1 text-sm text-slate-600">Anyone with this link can join the Northwind Studio workspace.</p>
  <div class="mt-10 flex items-stretch gap-2">
    <input id="ttp-invite" type="text" readonly value="northwind.example/join/7KQ2-M8TX" class="min-h-11 min-w-0 flex-1 rounded-lg border border-slate-300 bg-slate-50 px-3 font-mono text-sm text-slate-800 focus-visible:outline-2 focus-visible:outline-indigo-600">
    <div class="ttp-copy-wrap group relative">
      <button type="button" aria-label="Copy invite link" aria-describedby="ttp-copy-tip" class="ttp-copy grid size-11 place-items-center rounded-lg bg-indigo-600 text-white hover:bg-indigo-500 group-data-copied:bg-emerald-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5 group-data-copied:hidden"><path d="M9 9h10v11H9zM5 15V4h10"/></svg>
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" class="hidden size-5 group-data-copied:block"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>
      </button>
      <span id="ttp-copy-tip" role="tooltip" class="invisible absolute bottom-full right-0 z-10 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 group-data-copied:visible group-data-copied:opacity-100 motion-reduce:transition-none"><span class="ttp-copy-text relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white group-data-copied:bg-emerald-800">Copy link</span></span>
    </div>
  </div>
  <p class="ttp-copy-status sr-only" role="status"></p>
</div>

Full names only for truncated file links

  • truncate
  • ResizeObserver
  • hidden attribute
  • group-focus-within:

Hover or focus a control. Press Escape where the demo says so.

truncate cuts long file names with an ellipsis, and a tooltip repeats the full name only where it is actually cut. A ResizeObserver compares scrollWidth with clientWidth and toggles the tooltip hidden attribute and the aria-describedby reference, so short names get no redundant tooltip at any width.

View code
<div class="w-full max-w-sm rounded-2xl bg-white p-4 shadow-sm ring-1 ring-slate-900/5">
  <p class="px-2 pb-2 text-xs font-semibold uppercase tracking-wide text-slate-500">Recent files</p>
  <ul class="space-y-1">
    <li class="ttp-tr group relative flex items-center gap-3 rounded-lg px-2 py-2 hover:bg-slate-50">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-lg bg-rose-100 text-xs font-bold text-rose-800">PDF</span>
      <a href="#" class="ttp-tr-link min-w-0 truncate text-sm font-medium text-slate-800 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">Quarterly-brand-review-final-approved-by-legal-v7.pdf</a>
      <span role="tooltip" id="ttp-tr-1" class="ttp-tr-tip invisible absolute bottom-full left-12 right-0 z-10 pb-1 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block break-all rounded-md bg-slate-900 px-2.5 py-1.5 text-xs text-white">Quarterly-brand-review-final-approved-by-legal-v7.pdf</span></span>
    </li>
    <li class="ttp-tr group relative flex items-center gap-3 rounded-lg px-2 py-2 hover:bg-slate-50">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-lg bg-emerald-100 text-xs font-bold text-emerald-800">XLS</span>
      <a href="#" class="ttp-tr-link min-w-0 truncate text-sm font-medium text-slate-800 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">Retail-store-footfall-by-region-and-week-2026.xlsx</a>
      <span role="tooltip" id="ttp-tr-2" class="ttp-tr-tip invisible absolute bottom-full left-12 right-0 z-10 pb-1 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block break-all rounded-md bg-slate-900 px-2.5 py-1.5 text-xs text-white">Retail-store-footfall-by-region-and-week-2026.xlsx</span></span>
    </li>
    <li class="ttp-tr group relative flex items-center gap-3 rounded-lg px-2 py-2 hover:bg-slate-50">
      <span aria-hidden="true" class="grid size-9 shrink-0 place-items-center rounded-lg bg-sky-100 text-xs font-bold text-sky-800">DOC</span>
      <a href="#" class="ttp-tr-link min-w-0 truncate text-sm font-medium text-slate-800 hover:underline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600">Team-notes.docx</a>
      <span role="tooltip" id="ttp-tr-3" class="ttp-tr-tip invisible absolute bottom-full left-12 right-0 z-10 pb-1 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block break-all rounded-md bg-slate-900 px-2.5 py-1.5 text-xs text-white">Team-notes.docx</span></span>
    </li>
  </ul>
</div>

Toggletips, popovers and anchor positioning

Click-opened help built on the Popover API and aria-expanded, rich cards that hold links, and a tooltip that escapes an overflow container through the top layer and CSS anchor positioning.

Pricing toggletips with the Popover API

  • popover
  • popovertarget
  • [anchor-name:--*]
  • position-area

Click a trigger. Escape or a click outside closes it.

Each question-mark button opens a popover="auto" element through popovertarget, so the browser handles click, Escape, outside click and the expanded state with no script. Arbitrary properties name the anchor ([anchor-name:--ttp-seat]) and attach the popover to it, and a short @supports block places it above with position-area and flips it when space runs out. Browsers without anchor positioning keep the default centered popover. An after: pseudo-element enlarges the small button to a 44px target.

View code
<div class="w-full max-w-sm rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-900/5">
  <p class="text-sm font-semibold text-teal-800">Team plan</p>
  <p class="mt-2 text-4xl font-bold tracking-tight text-slate-900">$12<span class="text-base font-medium text-slate-500"> per seat / month</span></p>
  <ul class="mt-6 space-y-4 text-sm text-slate-700">
    <li class="flex items-center gap-2">
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5 text-teal-700"><path d="M8 13.2 4.8 10l-1.1 1.1L8 15.4l8.3-8.3-1.1-1.1z"/></svg>
      Unlimited seats
      <button type="button" popovertarget="ttp-seat" aria-label="What counts as a seat?" class="relative grid size-5 place-items-center rounded-full bg-slate-200 text-xs font-bold text-slate-700 [anchor-name:--ttp-seat] after:absolute after:-inset-3 hover:bg-slate-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">?</button>
    </li>
    <li class="flex items-center gap-2">
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5 text-teal-700"><path d="M8 13.2 4.8 10l-1.1 1.1L8 15.4l8.3-8.3-1.1-1.1z"/></svg>
      Guest access
      <button type="button" popovertarget="ttp-guest" aria-label="How does guest access work?" class="relative grid size-5 place-items-center rounded-full bg-slate-200 text-xs font-bold text-slate-700 [anchor-name:--ttp-guest] after:absolute after:-inset-3 hover:bg-slate-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">?</button>
    </li>
    <li class="flex items-center gap-2">
      <svg aria-hidden="true" viewBox="0 0 20 20" fill="currentColor" class="size-5 text-teal-700"><path d="M8 13.2 4.8 10l-1.1 1.1L8 15.4l8.3-8.3-1.1-1.1z"/></svg>
      180-day version history
    </li>
  </ul>
  <a href="#" class="mt-6 flex min-h-11 items-center justify-center rounded-lg bg-teal-700 text-sm font-semibold text-white hover:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Start a 14-day trial</a>
  <div id="ttp-seat" popover class="ttp-anchor-top w-64 max-w-[calc(100vw-2rem)] rounded-xl bg-slate-900 p-4 text-sm leading-6 text-slate-100 shadow-xl [position-anchor:--ttp-seat]">
    <p class="font-semibold text-white">What counts as a seat?</p>
    <p class="mt-1">Anyone who signs in during the billing month. Guests and deactivated members are free.</p>
  </div>
  <div id="ttp-guest" popover class="ttp-anchor-top w-64 max-w-[calc(100vw-2rem)] rounded-xl bg-slate-900 p-4 text-sm leading-6 text-slate-100 shadow-xl [position-anchor:--ttp-guest]">
    <p class="font-semibold text-white">Guest access</p>
    <p class="mt-1">Invite clients to single projects. They can comment and download, but cannot see the rest of the workspace.</p>
  </div>
</div>

Form field help toggletips

  • peer
  • peer-aria-expanded:
  • aria-expanded
  • role="status"

Click an i button. Escape or a click outside closes it.

The info button is the peer and the bubble follows it, so peer-aria-expanded:visible shows the bubble whenever the script sets aria-expanded="true". The bubble is a live region that gets its text only on open, which makes screen readers announce it, and Escape or a click elsewhere closes it. Unlike a hover tooltip, it works the same on touch screens.

View code
<form class="ttp-help-form w-full max-w-sm space-y-6 rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-900/5">
  <div>
    <div class="relative flex items-center gap-1.5">
      <label for="ttp-ws" class="text-sm font-semibold text-slate-900">Workspace URL</label>
      <button type="button" aria-expanded="false" aria-label="More about the workspace URL" data-help="Lowercase letters, numbers and hyphens only. You can change it later under Settings, General." class="ttp-help peer relative grid size-5 place-items-center rounded-full bg-sky-100 font-serif text-xs font-bold italic text-sky-800 after:absolute after:-inset-3 aria-expanded:bg-sky-700 aria-expanded:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">i</button>
      <span role="status" class="ttp-help-bubble invisible absolute left-0 right-0 top-full z-10 mt-2 rounded-lg bg-sky-950 px-3 py-2.5 text-sm leading-6 text-white shadow-lg peer-aria-expanded:visible"></span>
    </div>
    <div class="mt-2 flex rounded-lg border border-slate-300 focus-within:outline-2 focus-within:outline-sky-700">
      <span class="flex items-center rounded-l-lg bg-slate-50 px-3 text-sm text-slate-500">app.example/</span>
      <input id="ttp-ws" name="workspace" type="text" value="tide-and-pine" pattern="[a-z0-9\-]+" class="min-h-11 min-w-0 flex-1 rounded-r-lg px-3 text-sm text-slate-900 outline-none">
    </div>
  </div>
  <div>
    <div class="relative flex items-center gap-1.5">
      <label for="ttp-bill" class="text-sm font-semibold text-slate-900">Billing email</label>
      <button type="button" aria-expanded="false" aria-label="More about the billing email" data-help="Invoices and payment receipts go here. It does not need to be a member of the workspace." class="ttp-help peer relative grid size-5 place-items-center rounded-full bg-sky-100 font-serif text-xs font-bold italic text-sky-800 after:absolute after:-inset-3 aria-expanded:bg-sky-700 aria-expanded:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">i</button>
      <span role="status" class="ttp-help-bubble invisible absolute left-0 right-0 top-full z-10 mt-2 rounded-lg bg-sky-950 px-3 py-2.5 text-sm leading-6 text-white shadow-lg peer-aria-expanded:visible"></span>
    </div>
    <input id="ttp-bill" name="billing" type="email" placeholder="accounts@example.com" class="mt-2 block min-h-11 w-full rounded-lg border border-slate-300 px-3 text-sm text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-700">
  </div>
  <button type="submit" class="min-h-11 w-full rounded-lg bg-sky-700 text-sm font-semibold text-white hover:bg-sky-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-700">Create workspace</button>
</form>

Rich profile card popover with links

  • popover
  • popovertarget
  • position-try-fallbacks
  • focus-visible:

Click a trigger. Escape or a click outside closes it.

A tooltip may not hold links or buttons, so content like this belongs in a popover that opens on click. popover="auto" gives light dismiss and Escape for free, and the browser exposes the expanded state on the popovertarget button. The card is anchored below the name with position-area: bottom span-right and flips above when the frame runs out of room.

View code
<div class="w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5">
  <p class="text-sm font-semibold text-slate-900">Activity</p>
  <ul class="mt-4 space-y-4 text-sm leading-6 text-slate-600">
    <li>
      <button type="button" popovertarget="ttp-card-1" class="font-semibold text-fuchsia-800 underline decoration-fuchsia-300 decoration-2 underline-offset-4 [anchor-name:--ttp-card-1] hover:decoration-fuchsia-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">Leila Haddad</button>
      approved the homepage hero copy. <span class="text-slate-500">12 min ago</span>
    </li>
    <li>
      <button type="button" popovertarget="ttp-card-2" class="font-semibold text-fuchsia-800 underline decoration-fuchsia-300 decoration-2 underline-offset-4 [anchor-name:--ttp-card-2] hover:decoration-fuchsia-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">Jonah Pike</button>
      moved Pricing page to Ready for QA. <span class="text-slate-500">1 h ago</span>
    </li>
  </ul>
  <div id="ttp-card-1" popover class="ttp-anchor-card w-72 max-w-[calc(100vw-2rem)] rounded-2xl bg-white p-4 text-left shadow-xl ring-1 ring-slate-900/10 [position-anchor:--ttp-card-1]">
    <div class="flex items-center gap-3">
      <span aria-hidden="true" class="grid size-12 place-items-center rounded-full bg-fuchsia-700 text-base font-semibold text-white">LH</span>
      <div>
        <p class="font-semibold text-slate-900">Leila Haddad</p>
        <p class="text-sm text-slate-600">Content strategist, Lisbon</p>
      </div>
    </div>
    <p class="mt-3 text-sm text-slate-600">Owns the voice guide and the launch checklist.</p>
    <div class="mt-4 flex gap-2">
      <a href="#" class="flex min-h-10 flex-1 items-center justify-center rounded-lg bg-fuchsia-700 text-sm font-semibold text-white hover:bg-fuchsia-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">Message</a>
      <a href="#" class="flex min-h-10 flex-1 items-center justify-center rounded-lg border border-slate-300 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">View profile</a>
    </div>
  </div>
  <div id="ttp-card-2" popover class="ttp-anchor-card w-72 max-w-[calc(100vw-2rem)] rounded-2xl bg-white p-4 text-left shadow-xl ring-1 ring-slate-900/10 [position-anchor:--ttp-card-2]">
    <div class="flex items-center gap-3">
      <span aria-hidden="true" class="grid size-12 place-items-center rounded-full bg-cyan-800 text-base font-semibold text-white">JP</span>
      <div>
        <p class="font-semibold text-slate-900">Jonah Pike</p>
        <p class="text-sm text-slate-600">QA engineer, Toronto</p>
      </div>
    </div>
    <p class="mt-3 text-sm text-slate-600">Away until Monday. Ping Ada Lindqvist for urgent test runs.</p>
    <div class="mt-4 flex gap-2">
      <a href="#" class="flex min-h-10 flex-1 items-center justify-center rounded-lg bg-fuchsia-700 text-sm font-semibold text-white hover:bg-fuchsia-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">Message</a>
      <a href="#" class="flex min-h-10 flex-1 items-center justify-center rounded-lg border border-slate-300 text-sm font-semibold text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-fuchsia-700">View profile</a>
    </div>
  </div>
</div>

Tooltip that escapes a scrolling table

  • popover="manual"
  • anchor-name
  • overflow-x-auto
  • showPopover()

Hover or focus a status dot. Escape closes the tooltip.

An absolutely positioned tooltip inside overflow-x-auto would be clipped by the table edge. Here one shared popover="manual" element lives in the top layer, so no ancestor can clip it; the script shows it on hover or focus, moves anchor-name to the active button and closes it on Escape. Where CSS anchor positioning is missing, the same script falls back to getBoundingClientRect coordinates.

View code
<div class="w-full max-w-lg rounded-2xl bg-white shadow-sm ring-1 ring-slate-900/5">
  <p class="px-5 pt-5 text-sm font-semibold text-slate-900">Edge regions</p>
  <div class="mt-3 overflow-x-auto pb-2">
    <table class="w-full min-w-[30rem] text-left text-sm">
      <thead class="text-xs uppercase tracking-wide text-slate-500">
        <tr><th scope="col" class="px-5 py-2 font-semibold">Region</th><th scope="col" class="px-3 py-2 font-semibold">Status</th><th scope="col" class="px-3 py-2 font-semibold">p95 latency</th><th scope="col" class="px-5 py-2 font-semibold">Last deploy</th></tr>
      </thead>
      <tbody class="divide-y divide-slate-100 text-slate-700">
        <tr><td class="px-5 py-3 font-medium text-slate-900">Frankfurt</td><td class="px-3 py-1"><button type="button" class="ttp-dot grid size-11 place-items-center rounded-lg focus-visible:outline-2 focus-visible:outline-sky-700" aria-label="Healthy" data-tip="Healthy, 99.99% uptime this month"><span aria-hidden="true" class="size-3 rounded-full bg-emerald-600 ring-4 ring-emerald-100"></span></button></td><td class="px-3 py-3">42 ms</td><td class="px-5 py-3">Today 09:12</td></tr>
        <tr><td class="px-5 py-3 font-medium text-slate-900">Sรฃo Paulo</td><td class="px-3 py-1"><button type="button" class="ttp-dot grid size-11 place-items-center rounded-lg focus-visible:outline-2 focus-visible:outline-sky-700" aria-label="Degraded" data-tip="Degraded, packet loss on one upstream"><span aria-hidden="true" class="size-3 rounded-full bg-amber-500 ring-4 ring-amber-100"></span></button></td><td class="px-3 py-3">188 ms</td><td class="px-5 py-3">Yesterday 17:40</td></tr>
        <tr><td class="px-5 py-3 font-medium text-slate-900">Singapore</td><td class="px-3 py-1"><button type="button" class="ttp-dot grid size-11 place-items-center rounded-lg focus-visible:outline-2 focus-visible:outline-sky-700" aria-label="Healthy" data-tip="Healthy, 99.97% uptime this month"><span aria-hidden="true" class="size-3 rounded-full bg-emerald-600 ring-4 ring-emerald-100"></span></button></td><td class="px-3 py-3">61 ms</td><td class="px-5 py-3">Today 08:55</td></tr>
      </tbody>
    </table>
  </div>
  <div id="ttp-float" popover="manual" role="tooltip" class="ttp-float pointer-events-none m-0 w-max max-w-60 rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white shadow-lg"></div>
</div>

Shortcuts, charts and themed tooltips

Tooltips that carry data: keyboard shortcuts in kbd elements, chart values, inverted dark mode colors and status colors from prefixed @theme tokens.

Keyboard shortcut hints with kbd

  • <kbd>
  • aria-keyshortcuts
  • font-mono
  • group-focus-within:

Hover or focus an element to see its tooltip.

Each tooltip pairs the action name with its shortcut, styled as small kbd keys with a border and a bottom shadow. The button repeats the shortcut in aria-keyshortcuts so assistive technology can announce it without reading the tooltip twice. The muted key colors come straight from the stone palette.

View code
<header class="flex w-full max-w-xl flex-wrap items-center justify-between gap-3 rounded-2xl bg-stone-50 px-4 py-3 ring-1 ring-stone-200">
  <p class="text-sm font-semibold text-stone-900">Field notes <span class="font-normal text-stone-500">/ Moorland survey</span></p>
  <div class="flex gap-1">
    <div class="group relative">
      <button type="button" aria-label="Search" aria-keyshortcuts="Control+K" aria-describedby="ttp-kb-1" class="grid size-11 place-items-center rounded-lg text-stone-700 hover:bg-stone-200 focus-visible:outline-2 focus-visible:outline-emerald-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM20 20l-4-4"/></svg></button>
      <span id="ttp-kb-1" role="tooltip" class="invisible absolute right-0 top-full z-10 pt-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="flex items-center gap-2 whitespace-nowrap rounded-lg bg-white px-2.5 py-1.5 text-xs font-medium text-stone-800 shadow-lg ring-1 ring-stone-200">Search <span class="flex gap-0.5"><kbd class="rounded border border-stone-300 bg-stone-50 px-1.5 font-mono text-[11px] text-stone-700 shadow-[0_1px_0_var(--color-stone-300)]">Ctrl</kbd><kbd class="rounded border border-stone-300 bg-stone-50 px-1.5 font-mono text-[11px] text-stone-700 shadow-[0_1px_0_var(--color-stone-300)]">K</kbd></span></span></span>
    </div>
    <div class="group relative">
      <button type="button" aria-label="New note" aria-keyshortcuts="Alt+N" aria-describedby="ttp-kb-2" class="grid size-11 place-items-center rounded-lg text-stone-700 hover:bg-stone-200 focus-visible:outline-2 focus-visible:outline-emerald-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M12 5v14M5 12h14"/></svg></button>
      <span id="ttp-kb-2" role="tooltip" class="invisible absolute right-0 top-full z-10 pt-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="flex items-center gap-2 whitespace-nowrap rounded-lg bg-white px-2.5 py-1.5 text-xs font-medium text-stone-800 shadow-lg ring-1 ring-stone-200">New note <span class="flex gap-0.5"><kbd class="rounded border border-stone-300 bg-stone-50 px-1.5 font-mono text-[11px] text-stone-700 shadow-[0_1px_0_var(--color-stone-300)]">Alt</kbd><kbd class="rounded border border-stone-300 bg-stone-50 px-1.5 font-mono text-[11px] text-stone-700 shadow-[0_1px_0_var(--color-stone-300)]">N</kbd></span></span></span>
    </div>
    <div class="group relative">
      <button type="button" aria-label="Share" aria-keyshortcuts="Control+Shift+S" aria-describedby="ttp-kb-3" class="grid size-11 place-items-center rounded-lg bg-emerald-800 text-white hover:bg-emerald-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M12 4v11M7.5 8.5 12 4l4.5 4.5M5 14v5h14v-5"/></svg></button>
      <span id="ttp-kb-3" role="tooltip" class="invisible absolute right-0 top-full z-10 pt-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="flex items-center gap-2 whitespace-nowrap rounded-lg bg-white px-2.5 py-1.5 text-xs font-medium text-stone-800 shadow-lg ring-1 ring-stone-200">Share <span class="flex gap-0.5"><kbd class="rounded border border-stone-300 bg-stone-50 px-1.5 font-mono text-[11px] text-stone-700 shadow-[0_1px_0_var(--color-stone-300)]">Ctrl</kbd><kbd class="rounded border border-stone-300 bg-stone-50 px-1.5 font-mono text-[11px] text-stone-700 shadow-[0_1px_0_var(--color-stone-300)]">Shift</kbd><kbd class="rounded border border-stone-300 bg-stone-50 px-1.5 font-mono text-[11px] text-stone-700 shadow-[0_1px_0_var(--color-stone-300)]">S</kbd></span></span></span>
    </div>
  </div>
</header>

Bar chart with value tooltips

  • h-[*%]
  • group-hover:
  • tabular-nums
  • sr-only

Hover or focus an element to see its tooltip.

Each bar is a group sized with an arbitrary height such as h-[78%], with a transparent full-size button for focus and a top tooltip that shows the exact figure. The peak bar uses a stronger color, and tabular-nums keeps the digits aligned. Every bar is reachable by Tab, so the values are not locked behind a mouse.

View code
<figure class="w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5">
  <figcaption class="flex items-baseline justify-between">
    <span class="text-sm font-semibold text-slate-900">Weekly sign-ups</span>
    <span class="text-sm tabular-nums text-slate-600">9,382 total</span>
  </figcaption>
  <div class="mt-12 flex h-44 items-end gap-2 border-b border-slate-200">
    <div class="group relative h-[45%] flex-1 rounded-t-md bg-indigo-200 hover:bg-indigo-300 has-focus-visible:bg-indigo-300">
      <button type="button" aria-label="Monday" aria-describedby="ttp-ch-1" class="absolute inset-0 rounded-t-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-700"></button>
      <span id="ttp-ch-1" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs tabular-nums text-white">1,180</span></span>
    </div>
    <div class="group relative h-[58%] flex-1 rounded-t-md bg-indigo-200 hover:bg-indigo-300 has-focus-visible:bg-indigo-300">
      <button type="button" aria-label="Tuesday" aria-describedby="ttp-ch-2" class="absolute inset-0 rounded-t-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-700"></button>
      <span id="ttp-ch-2" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs tabular-nums text-white">1,512</span></span>
    </div>
    <div class="group relative h-[52%] flex-1 rounded-t-md bg-indigo-200 hover:bg-indigo-300 has-focus-visible:bg-indigo-300">
      <button type="button" aria-label="Wednesday" aria-describedby="ttp-ch-3" class="absolute inset-0 rounded-t-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-700"></button>
      <span id="ttp-ch-3" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs tabular-nums text-white">1,355</span></span>
    </div>
    <div class="ttp-peak group relative h-[78%] flex-1 rounded-t-md bg-indigo-600 hover:bg-indigo-700 has-focus-visible:bg-indigo-700">
      <button type="button" aria-label="Thursday, highest day" aria-describedby="ttp-ch-4" class="absolute inset-0 rounded-t-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-700"></button>
      <span id="ttp-ch-4" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs tabular-nums text-white">2,047</span></span>
    </div>
    <div class="group relative h-[64%] flex-1 rounded-t-md bg-indigo-200 hover:bg-indigo-300 has-focus-visible:bg-indigo-300">
      <button type="button" aria-label="Friday" aria-describedby="ttp-ch-5" class="absolute inset-0 rounded-t-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-700"></button>
      <span id="ttp-ch-5" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs tabular-nums text-white">1,664</span></span>
    </div>
    <div class="group relative h-[30%] flex-1 rounded-t-md bg-indigo-200 hover:bg-indigo-300 has-focus-visible:bg-indigo-300">
      <button type="button" aria-label="Saturday" aria-describedby="ttp-ch-6" class="absolute inset-0 rounded-t-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-700"></button>
      <span id="ttp-ch-6" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs tabular-nums text-white">804</span></span>
    </div>
    <div class="group relative h-[32%] flex-1 rounded-t-md bg-indigo-200 hover:bg-indigo-300 has-focus-visible:bg-indigo-300">
      <button type="button" aria-label="Sunday" aria-describedby="ttp-ch-7" class="absolute inset-0 rounded-t-md focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-700"></button>
      <span id="ttp-ch-7" role="tooltip" class="invisible absolute bottom-full right-0 z-10 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-slate-900 px-2 py-1 text-xs tabular-nums text-white">820</span></span>
    </div>
  </div>
  <div aria-hidden="true" class="mt-2 flex gap-2 text-center text-xs text-slate-500">
    <span class="flex-1">M</span><span class="flex-1">T</span><span class="flex-1">W</span><span class="flex-1">T</span><span class="flex-1">F</span><span class="flex-1">S</span><span class="flex-1">S</span>
  </div>
</figure>

Inverted tooltips on a dark player

  • dark:
  • dark:bg-white
  • aria-pressed:
  • @custom-variant dark

Hover or focus an element to see its tooltip.

The tooltips invert the surface: slate-900 with white text on light pages and dark:bg-white dark:text-slate-900 under the class-based dark variant, so they always stand out from the panel behind them. The arrow switches color with the same dark: utilities. The play button is a toggle with aria-pressed and swaps its tooltip text from the script.

View code
<div class="w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5 dark:bg-slate-900 dark:ring-white/10">
  <div class="flex items-center gap-4">
    <img src="snow-camp.webp" width="800" height="600" alt="Orange tents pitched on snow below a mountain ridge" class="size-16 shrink-0 rounded-xl object-cover">
    <div class="min-w-0">
      <p class="truncate text-sm font-semibold text-slate-900 dark:text-white">Episode 48: Winter camps</p>
      <p class="text-sm text-slate-600 dark:text-slate-400">Trail Notes podcast</p>
    </div>
  </div>
  <div class="mt-5 h-1.5 rounded-full bg-slate-200 dark:bg-slate-700"><div class="h-full w-2/5 rounded-full bg-rose-500 dark:bg-rose-400"></div></div>
  <div class="mt-1 flex justify-between text-xs tabular-nums text-slate-500 dark:text-slate-400"><span>14:02</span><span>35:40</span></div>
  <div class="mt-4 flex items-center justify-center gap-3">
    <div class="group relative">
      <button type="button" aria-label="Back 15 seconds" aria-describedby="ttp-pl-1" class="grid size-11 place-items-center rounded-full text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800 focus-visible:outline-2 focus-visible:outline-rose-400"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M4 12a8 8 0 1 0 2.3-5.7M4 4v4h4"/></svg></button>
      <span id="ttp-pl-1" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white dark:bg-white dark:text-slate-900">Back 15 s<span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900 dark:bg-white"></span></span></span>
    </div>
    <div class="ttp-play-wrap group relative">
      <button type="button" aria-pressed="false" aria-label="Play" aria-describedby="ttp-pl-2" class="ttp-play grid size-14 place-items-center rounded-full bg-rose-600 text-white hover:bg-rose-500 dark:bg-rose-500 dark:text-slate-950 dark:hover:bg-rose-400 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-400">
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" class="size-6 group-has-aria-pressed:hidden"><path d="M8 5v14l11-7z"/></svg>
        <svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" class="hidden size-6 group-has-aria-pressed:block"><path d="M7 5h4v14H7zM13 5h4v14h-4z"/></svg>
      </button>
      <span id="ttp-pl-2" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white dark:bg-white dark:text-slate-900"><span class="ttp-play-label">Play</span> <kbd class="font-mono text-[11px] text-slate-300 dark:text-slate-600">Space</kbd><span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900 dark:bg-white"></span></span></span>
    </div>
    <div class="group relative">
      <button type="button" aria-label="Forward 30 seconds" aria-describedby="ttp-pl-3" class="grid size-11 place-items-center rounded-full text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800 focus-visible:outline-2 focus-visible:outline-rose-400"><svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="size-5"><path d="M20 12a8 8 0 1 1-2.3-5.7M20 4v4h-4"/></svg></button>
      <span id="ttp-pl-3" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="relative block whitespace-nowrap rounded-md bg-slate-900 px-2.5 py-1.5 text-xs font-medium text-white dark:bg-white dark:text-slate-900">Forward 30 s<span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900 dark:bg-white"></span></span></span>
    </div>
  </div>
</div>

Status tooltips from @theme tokens

  • @theme
  • bg-ttp-ok
  • text-ttp-warn-ink
  • group-focus-within:

Hover or focus an element to see its tooltip.

A prefixed @theme block adds --color-ttp-ok, --color-ttp-warn, --color-ttp-fail and an ink color, and Tailwind turns them into bg-ttp-ok, text-ttp-warn-ink and the rest. Each pipeline step gets a tooltip in its own status color, with dark ink on amber to keep the contrast above 4.5:1. Renaming a token in one place recolors every tooltip.

View code
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-900/5">
  <div class="flex items-center justify-between">
    <p class="text-sm font-semibold text-slate-900">Pipeline #3187 <span class="font-normal text-slate-500">main</span></p>
    <span class="rounded-full bg-red-50 px-2.5 py-1 text-xs font-semibold text-ttp-fail">Failed</span>
  </div>
  <ol class="mt-14 flex items-center" aria-label="Pipeline steps">
    <li class="group relative">
      <button type="button" aria-describedby="ttp-st-1" class="flex min-h-11 items-center gap-2 rounded-lg px-2 text-sm font-medium text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-slate-700"><span aria-hidden="true" class="size-3 rounded-full bg-ttp-ok"></span>Build</button>
      <span id="ttp-st-1" role="tooltip" class="invisible absolute bottom-full left-0 z-10 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-ttp-ok px-2.5 py-1.5 text-xs font-medium text-white">Passed in 1m 48s</span></span>
    </li>
    <li aria-hidden="true" class="h-px min-w-3 flex-1 bg-slate-200"></li>
    <li class="group relative">
      <button type="button" aria-describedby="ttp-st-2" class="flex min-h-11 items-center gap-2 rounded-lg px-2 text-sm font-medium text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-slate-700"><span aria-hidden="true" class="size-3 rounded-full bg-ttp-warn"></span>Test</button>
      <span id="ttp-st-2" role="tooltip" class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block whitespace-nowrap rounded-md bg-ttp-warn px-2.5 py-1.5 text-xs font-medium text-ttp-warn-ink">2 flaky tests retried</span></span>
    </li>
    <li aria-hidden="true" class="h-px min-w-3 flex-1 bg-slate-200"></li>
    <li class="group relative">
      <button type="button" aria-describedby="ttp-st-3" class="flex min-h-11 items-center gap-2 rounded-lg px-2 text-sm font-medium text-slate-800 hover:bg-slate-50 focus-visible:outline-2 focus-visible:outline-slate-700"><span aria-hidden="true" class="size-3 rounded-full bg-ttp-fail"></span>Deploy</button>
      <span id="ttp-st-3" role="tooltip" class="invisible absolute bottom-full right-0 z-10 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 motion-reduce:transition-none"><span class="block w-max max-w-52 rounded-md bg-ttp-fail px-2.5 py-1.5 text-xs font-medium leading-5 text-white">Health check timed out on eu-west</span></span>
    </li>
  </ol>
</div>

How to Build a Tailwind Tooltip With group-hover

The tooltip is a plain span that stays in the DOM the whole time. The wrapper carries group, the span is absolutely positioned, and hovering the wrapper is what makes the span visible.

It behaves like any tooltip in UI design. A short label appears next to a control and goes away once attention moves elsewhere.

Start at the wrapper and work outward.

  1. Wrap everything in a div with relative inline-block group.
  2. Add the trigger, usually a button, as the first child.
  3. Add the tooltip as a sibling span with absolute, plus bottom-full left-1/2 -translate-x-1/2 for a top placement.
  4. Hide it with invisible opacity-0.
  5. Reveal it with group-hover:visible group-hover:opacity-100.
  6. Add transition-opacity so the reveal is a fade instead of a snap.

Put together, the markup looks like this:

<div class="relative inline-block group">
  <button type="button" class="rounded bg-slate-800 px-3 py-2 text-sm text-white">
    Save
    </button>
  <span
    class="invisible absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-slate-900 px-2 py-1 text-xs text-white opacity-0 transition-opacity group-hover:visible group-hover:opacity-100"
    >
  Save changes
  </span>
</div>

Tailwind’s documentation uses the same invisible plus group-hover:visible pairing in its named-group example (Tailwind CSS v4.3 docs).

This version reacts to hover only. Keyboard focus and touch need extra classes.

A short label is fine with whitespace-nowrap. Longer text wants w-max max-w-xs instead, so the box wraps at a readable width.

z-10 lifts the tooltip over its neighbors, but it can’t get out of a parent that clips overflow. More on that near the end.

You’ll also see pointer-events-none added to stop the tooltip catching the pointer and flickering. It works, but nobody can move onto the tooltip to read it anymore, and the accessibility section comes back to that.

Tailwind Tooltip Placement and Arrow Classes

Each side comes down to two moves. Push the tooltip off the trigger’s edge, then center it along the other axis.

Placement classes per side

  • A top tooltip takes bottom-full left-1/2 -translate-x-1/2
  • Below the trigger, it’s top-full left-1/2 -translate-x-1/2
  • To the left, use right-full top-1/2 -translate-y-1/2
  • left-full top-1/2 -translate-y-1/2 handles the right side

Spacing is where most snippets go wrong. A margin like mb-2 leaves a strip that belongs to neither the trigger nor the tooltip, so the pointer drops the group while crossing it.

Padding on the tooltip wrapper fixes that (pb-2 for a top tooltip). The strip then sits inside the group and hover survives the trip.

Arrow as rotated square or pseudo-element

Tailwind’s documentation favors a real element over a pseudo-element for most decorative shapes, and keeps before: and after: for content that should stay out of the DOM (Tailwind CSS v4.3 docs).

For an arrow, that means a rotated square. Give it absolute size-2 rotate-45 and the same background color as the tooltip box.

The edge class depends on the side. A top tooltip gets top-full on the arrow, a bottom one gets bottom-full, a left one left-full and a right one right-full. Vertical placements pair that with left-1/2 -translate-x-1/2, and horizontal ones with top-1/2 -translate-y-1/2.

Here is a top tooltip with the padded wrapper and the arrow combined:

<span class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-opacity group-hover:visible group-hover:opacity-100">
<span class="relative block whitespace-nowrap rounded bg-slate-900 px-2 py-1 text-xs text-white">
Save changes
  <span aria-hidden="true" class="absolute left-1/2 top-full size-2 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-slate-900"></span>
</span>
</span>

The arrow carries no meaning, so it gets aria-hidden="true".

The pseudo-element route skips the extra span and builds a border triangle with before: classes, the same trick behind CSS arrows in plain stylesheets. Tailwind adds content: '' to before: automatically, so only the border classes are needed: before:absolute before:top-full before:left-1/2 before:-translate-x-1/2 before:border-4 before:border-transparent before:border-t-slate-900.

How Do You Animate a Tailwind Tooltip and Style It for Dark Mode?

The fade comes from transition-opacity on the base state. Add group-hover:delay-300 if the reveal should wait, and use dark: variants for color. Tailwind’s transition-property reference gives 150 ms as the default duration, which already suits a label this small.

A plain invisible opacity-0 transition-opacity setup fades in but cuts out. On mouse leave, visibility flips to hidden at once, so the opacity fade runs on an element nobody can see.

Listing visibility in the transition solves it: transition-[opacity,visibility]. MDN documents that visibility interpolates between visible and not-visible as a discrete step, so the element stays visible for the whole transition as long as one end of it is visible.

That’s also why invisible beats hidden here. Tailwind’s transition-discrete utility is built for hidden-to-block changes, and a tooltip gains nothing from the extra class.

group-hover:delay-300 postpones only the reveal. The hide runs without delay, because the state being entered supplies the delay value. Tailwind’s transition-delay docs apply delays conditionally the same way, with variants such as motion-reduce:delay-0.

People who ask their system for less motion get motion-reduce:transition-none, which removes the fade since the variant maps to prefers-reduced-motion: reduce.

Slide-in effects changed in v4. Translate, scale and rotate are built on individual CSS properties now, so transition-[opacity,transform] no longer animates them. List translate instead, as in transition-[opacity,translate,visibility] (Tailwind CSS upgrade guide).

The dark: variant follows prefers-color-scheme by default, per Tailwind’s variant reference. A class or data-attribute toggle needs a custom variant registered with @custom-variant.

I’d invert the tooltip rather than reuse the page palette. So bg-slate-900 text-white in light mode, and dark:bg-slate-100 dark:text-slate-900 in dark mode.

Tooltip text is small text, so the color contrast of each pair has to clear 4.5:1. W3C’s Understanding document for success criterion 1.4.3 confirms the rule covers text that appears on hover or keyboard focus.

Does a Tailwind Tooltip Work on Touch Screens and Keyboards?

Not by default. Tailwind CSS v4 applies the hover variant only on devices that support hover, so hover-only tooltips stay closed on touch screens, and keyboard users see nothing until focus variants are added.

The v4 upgrade guide describes the change. hover now compiles inside a media query on the hover feature, @media (hover: hover), and Tailwind’s own variant reference for v4.3 lists it that way.

Pull request 8394 in the tailwindlabs repository introduced the behavior behind a future flag in v3 and called it a breaking change for menus that open on hover. It names hidden group-hover:block as the pattern that stops working.

Read together, the two sources describe a regression that tutorials rarely mention. Under v3, a tap left a sticky hover state that opened tooltips on phones. Under v4, the same markup stays shut.

The upgrade guide’s advice is to treat hover as an enhancement, because touch devices can’t truly hover. A hover-only tooltip can decorate. It shouldn’t be the only place a fact lives.

Focus variants sit outside the hover media query (focus compiles to a plain &:focus selector), so a keyboard path can be added without touching hover.

There’s a snag with group-focus, though. It fires only when the element carrying the group class is itself focused, which is why Tailwind’s older docs put group on the button. A button inside a wrapper needs group-focus-within instead, which matches when the wrapper or any descendant has focus.

ProblemFixTrade-off
Tooltip never opens on tapShow required text by default, keep the tooltip as extraNeeds layout room for visible text
Tooltip ignores keyboard focusAdd group-focus-within:visible and group-focus-within:opacity-100Also opens after a mouse click focuses the button
Old tap-opens-tooltip behavior is neededOverride with @custom-variant hover (&:hover)Brings sticky hover back on touch

To limit the reveal to keyboard focus, swap in group-has-[:focus-visible], which checks the focus-visible state of any descendant.

Touch needs a click-driven toggle in place of hover.

  • Give the button peer and aria-expanded="false"
  • Style the tooltip, placed after the button, with peer-aria-expanded:visible peer-aria-expanded:opacity-100
  • Flip the attribute in a click handler

People usually call that a toggletip. It stays open until the user toggles it again, so it also needs an Escape or outside-click handler to close.

Tailwind Tooltip Accessibility: What WCAG 1.4.13 and ARIA Require

Level AA is where WCAG 1.4.13 sits, and it asks for hover or focus content that can be dismissed, hovered and kept on screen. The W3C ARIA Authoring Practices pattern covers the markup, with role="tooltip" on the container and aria-describedby on the trigger.

The W3C’s Understanding document for the criterion (updated 12 July 2026) lists custom tooltips among the content it covers. Out of the box, a plain group-hover tooltip satisfies one of the three conditions.

ConditionPlain group-hover resultFix
DismissibleFails: CSS alone has no Escape handlerKeydown script that sets the hidden attribute
HoverableFails with a margin gap or pointer-events-nonePadded wrapper, no pointer-events-none
PersistentPasses while hover or focus lastsAdd group-focus-within, avoid timed hiding

Hoverable is the row people fail by accident. The W3C lists hover content that can’t itself be hovered as failure F95.

Dismissible has two exemptions, for input error messages and for content that doesn’t obscure or replace anything else. A tooltip floating over empty space can claim it, but most sit on top of something, so plan for Escape.

The ARIA half of the pattern is two attributes. The tooltip gets role="tooltip" and a unique id, and the trigger points at that id with aria-describedby.

<div class="relative inline-block group" data-tooltip-group>
  <button type="button" aria-describedby="save-tip" class="rounded bg-slate-800 px-3 py-2 text-sm text-white">
    Save
    </button>
  <span
    id="save-tip"
    role="tooltip"
    class="invisible absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-[opacity,visibility] group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100"
    >
  <span class="block whitespace-nowrap rounded bg-slate-900 px-2 py-1 text-xs text-white">Save changes</span>
</span>
</div>

The APG page marks the pattern as work in progress without task force consensus. Its notes are still specific. Focus stays on the trigger, the tooltip never takes it, and the tooltip closes on blur or Escape.

A few lines of script finish the job. Escape hides every tooltip, and hover or focus on the wrapper brings the tooltip back.

document.addEventListener('keydown', (event) => {
  if (event.key !== 'Escape') return;
  document.querySelectorAll('[role="tooltip"]').forEach((tip) => {
    tip.hidden = true;
  });
});

document.querySelectorAll('[data-tooltip-group]').forEach((group) => {
  const tip = group.querySelector('[role="tooltip"]');
  ['mouseenter', 'focusin'].forEach((type) => {
    group.addEventListener(type, () => { tip.hidden = false; });
  });
});

The tooltip carries no display utility, so the native hidden attribute wins in v3 and v4 alike. Tailwind v4 also made the attribute outrank display classes such as block and flex (Tailwind upgrade guide).

One limit worth knowing: a tooltip can’t hold links or buttons. The APG says a hover that contains focusable elements should be built as a non-modal dialog, and the W3C adds that more predictable ways of putting content on a page usually exist.

Pure CSS, daisyUI, Flowbite or Floating UI for a Tailwind Tooltip

For a handful of tooltips with room around them, plain utility classes are enough. daisyUI makes sense when the project already runs it and the text is plain. Flowbite fits if script-managed triggers are acceptable, and Floating UI or Radix are the pick when collision handling and Escape need to come built in.

MethodDependencyPlacement and collisionsKeyboard and Escape
Pure utilitiesNoneHand-set classes, no flipgroup-focus-within plus a small script
daisyUITailwind pluginFour sides, three alignments, no flip option documentedDocs mention hover only
FlowbiteFlowbite JS, Popper.js arrow markupFour sides via data attributeDocs say hover or focus
Floating UIJavaScript libraryoffset, flip, shift and arrow middlewareYou wire events or hooks
Radix UI and shadcn/uiReact package (shadcn/ui can be built on Radix UI, Base UI or React Aria)Side propOpens on focus, closes on Escape

Reach for a library last. Installing a kit to label a row of icon buttons trades a few utility classes for a dependency, and with Flowbite for a script tag too.

Pure CSS with utility classes

Utility classes do what a plain CSS tooltip does with a hand-written stylesheet, minus the stylesheet. Nothing to install and nothing to keep updated.

You get no dependency, no script for the basic version, and behavior you can read straight from the HTML. What you give up is collision handling, and Escape dismissal still needs a script.

Component kits (daisyUI and Flowbite)

daisyUI gives you the tooltip class and a data-tip attribute. That produces a tooltip with four placements (top is the default), start, center and end alignment, a tooltip-open modifier to force it open, and color modifiers. The text lives in an attribute unless you add the tooltip-content part, and the docs describe hover only, so test keyboard focus before shipping.

Flowbite works through data-tooltip-target, which wires a trigger to a tooltip element whose markup already carries role="tooltip". Attributes switch placement and a hover or click trigger. None of it runs until the Flowbite script is on the page.

Setup differs in v4. daisyUI installs with npm i -D daisyui@latest and one line after the Tailwind import, @plugin "daisyui"; (daisyUI docs, v5.7.50). Flowbite’s v4.0 quickstart adds @import "flowbite/src/themes/default";, @plugin "flowbite/plugin";, an @source line for its package folder, and the flowbite.min.js script.

Floating UI and Radix-based tooltips

Radix’s TooltipProvider waits 700 ms by default before opening a tooltip, according to the Radix UI documentation, and the delay can be changed globally or per tooltip.

Radix opens on focus or hover and closes on Escape or when the trigger is activated. shadcn/ui offers its tooltip as a component built on that kind of primitive (its current docs cover Radix UI, Base UI and React Aria versions). Add it with the CLI, mount TooltipProvider once at the app root, and set placement with the side prop.

Escape, focus and delay behavior arrive built in with Radix. Floating UI is the placement tool, with offset, flip, shift and arrow middleware plus React and Vue bindings next to the plain DOM package. Both add JavaScript and take placement out of the class list, and Radix and shadcn/ui only serve React projects.

One trap sits inside the Floating UI docs. The tutorial example lists flip() before shift(), while the combining section of the flip page puts shift() first for placements such as top and flip() first for edge-aligned ones such as top-start.

The flip page says its recommended order gives the most expected positioning whatever placement is used. Follow it over the tutorial.

When Does a Pure CSS Tailwind Tooltip Break?

Clipping comes first. An ancestor with overflow-hidden, overflow-auto or overflow-scroll cuts an absolutely positioned tooltip off at its own edge, and table cells, cards and scrolling sidebars are the usual culprits.

Screen edges are next. The side is fixed when you write the classes, so nothing moves the tooltip when space runs out.

Stacking is the quiet one. z-10 only competes inside one stacking context, so a tooltip inside a transformed or positioned parent can still render under a neighboring section.

The edge case is the one no utility class solves, because a class can’t measure how much room is left in the viewport. Floating UI’s flip() exists for exactly that. It changes the placement to keep the floating element in view, flipping to the opposite side by default (Floating UI docs).

A portal handles clipping and stacking. Radix renders tooltip content through Tooltip.Portal, outside the trigger’s DOM tree, so ancestor overflow rules no longer cut it.

CSS anchor positioning moves the placement job into CSS, with anchor-name on the trigger and position-anchor on the tooltip. Whether it’s safe to rely on depends on which source you read.

  • web.dev said in January 2026 that Firefox 147 made CSS anchor positioning Baseline Newly available.
  • The same web.dev page, fetched in October 2026, has a position-anchor support table listing Chrome 151, Edge 151, Firefox 151 and Safari 27, where an earlier snapshot listed Chrome 125, Firefox 147 and Safari 26. MDN’s compat data marks the earlier versions as partial only because the property’s initial value changed.
  • OddBird said in April 2026 that anchor positioning was not Baseline yet.
  • The web-features data behind Baseline badges now lists anchor positioning as not Baseline, after more anchor properties were added to the feature in May 2026.

The web.dev post warns that its MDN-sourced browser data can trail recent releases. Issue 3558 in the web-features repository explains that the Baseline status was then computed from anchor-name, position-anchor and anchor() only, so different dates and different property sets explain the gap.

Tailwind’s v4 upgrade guide sets its own browser floor at Safari 16.4, Chrome 111 and Firefox 128. Every anchor positioning version quoted above (Chrome 125, Firefox 147 and Safari 26 for anchor-name) is higher, so a project that supports v4’s full range still has a cross-browser compatibility gap to cover.

What I’d do is keep the group-hover tooltip as the baseline, layer anchor rules behind Tailwind’s supports-[...] variant (which generates an @supports rule), and bring in Floating UI only where the fallback matters.

Tailwind Tooltip FAQ

What separates a tooltip from a toggletip or a popover?

A tooltip opens on hover or keyboard focus, holds a short non-interactive label, and never takes focus. A toggletip opens on click or tap from a button.

Once the content includes links or buttons, you’re in popover territory, which the W3C ARIA Authoring Practices assigns to the non-modal dialog pattern.

Is the native title attribute enough?

Only for a plain browser-drawn hint. The browser controls how title tooltips look and behave, so Tailwind classes can’t restyle them or change their delay.

WCAG 1.4.13 exempts them for that reason, since the user agent controls the presentation (W3C).

How do you build a Tailwind tooltip in React?

Reuse the same markup with className in place of class. A hover-and-focus version built on group-hover needs no state at all.

Escape dismissal or a click toggle needs a useState hook, or the shadcn/ui Tooltip component, which is built on Radix UI, Base UI or React Aria depending on the version you add.

Can a Tailwind tooltip work on a disabled button?

Yes, with a wrapper. The shadcn/ui docs show the fix, which is to wrap the button in a span and attach the tooltip to the span.

Disabled buttons can’t take keyboard focus, so aria-disabled keeps the trigger reachable when keyboard users need the tooltip text.

What to Test Before a Tailwind Tooltip Ships

Run Tab focus, Escape, pointer movement onto the tooltip, touch emulation and 200% zoom, in that order.

Tab focus goes first because every other check needs a tooltip that opens. Escape and the pointer path come next, before any visual polish, and touch plus zoom wait until the layout stops changing.

Passing all five still leaves edge collisions unsolved. Staying with utility classes plus one Escape script means reviewing placement by hand for every tooltip near a screen edge.

Position verified 29 October 2026. The advice changes once CSS anchor positioning is Baseline again in the web-features data, because native placement would remove that review cost.

Other hover and focus patterns follow the same rules, and a web accessibility checklist covers them in more depth.