tooltip-wc

Progressive enhancement tooltips: native title fallback upgraded to styled popovers with arrows and positioning.

Overview

Tooltips use a title-first progressive enhancement approach. The title attribute provides a no-JS fallback (native browser tooltip). When JavaScript loads, the init script reads the title, removes it, and creates a styled popover with arrows and positioning.

<!-- No JS: native browser tooltip shows "This is a tooltip!" --> <!-- With JS: title removed, styled popover tooltip created --> <button title="This is a tooltip!" data-tooltip>Hover me</button>

Progressive Enhancement

State What the user sees
No JavaScript Native browser tooltip from title attribute
JavaScript loaded Styled popover with arrow, positioned via CSS Anchor or JS fallback

Simple Text Tooltip (Tier 1)

Add title and data-tooltip (no value) to any element. The init script creates the popover automatically.

Positions

Use data-tooltip-position to control where the tooltip appears relative to the trigger.

<button title="Tooltip appears above" data-tooltip>Top</button> <button title="Tooltip appears below" data-tooltip data-tooltip-position="bottom">Bottom</button> <button title="Tooltip appears on the left" data-tooltip data-tooltip-position="left">Left</button> <button title="Tooltip appears on the right" data-tooltip data-tooltip-position="right">Right</button>

With Different Elements

Tooltips work on any focusable element: buttons, links, and elements with tabindex.

<!-- On a link --> <a href="#" title="Links can have tooltips too!" data-tooltip>Hover this link</a> <!-- On an icon button --> <button type="button" class="ghost" aria-label="Settings" title="Open settings" data-tooltip data-tooltip-position="bottom"> <icon-wc name="settings"></icon-wc> </button> <!-- On text with tabindex --> <span tabindex="0" title="Additional information about this feature" data-tooltip style="text-decoration: underline dotted; cursor: help;"> What's this? </span>

Attributes

Attribute On Description
title trigger Tooltip text. Read by init script and removed to prevent native double-tooltip.
data-tooltip trigger Marker for init script discovery. No value = create from title. With value = reference popover by ID.
data-tooltip-position trigger Position: top (default), bottom, left, or right.

Rich HTML Tooltip (Tier 2)

For tooltips with formatted content (keyboard shortcuts, icons, multiple lines), use data-tooltip="id" to reference an existing popover element. The title provides a plain-text no-JS fallback.

<button type="button" title="Save (Ctrl+S)" data-tooltip="save-tip"> <icon-wc name="save" size="sm"></icon-wc> Save </button> <div id="save-tip" popover="hint" role="tooltip"> <strong>Save document</strong><br> <kbd>Ctrl</kbd> + <kbd>S</kbd> <span class="tooltip-arrow" aria-hidden="true"></span> </div> <button type="button" title="Copy to clipboard" data-tooltip="copy-tip"> Copy </button> <div id="copy-tip" popover="hint" role="tooltip"> Copy to clipboard<br> <small>Copied items expire after 24 hours</small> <span class="tooltip-arrow" aria-hidden="true"></span> </div>

Card Variant (Tier 3)

For richer content previews like user profiles or link previews, use <tooltip-wc data-variant="card">. Cards use popover="manual" for interactive content and have longer show/hide delays.

<tooltip-wc data-variant="card"> <a href="/user/jane" data-trigger>Jane Smith</a> <div data-content> <div style="display: flex; align-items: center; gap: var(--size-s);"> <user-avatar data-size="lg"> <span data-fallback>JS</span> </user-avatar> <div> <strong>Jane Smith</strong> <span class="text-muted" style="display: block;"> Senior Developer </span> </div> </div> <span class="text-muted" style="display: block; margin-block-start: var(--size-xs);"> <icon-wc name="map-pin" size="xs"></icon-wc> San Francisco &middot; 142 contributions </span> </div> </tooltip-wc> <tooltip-wc data-variant="card"> <a href="/docs/elements/native/dialog/" data-trigger>dialog element</a> <div data-content> <div style="display: flex; gap: var(--size-s); align-items: start;"> <icon-wc name="layout-template" size="lg" style="color: var(--color-interactive); flex-shrink: 0;"> </icon-wc> <div> <strong>Dialog Element</strong> <span class="text-muted" style="display: block; margin-block-start: var(--size-2xs);"> Native modal and non-modal dialogs with backdrop, keyboard handling, and focus management. </span> </div> </div> </div> </tooltip-wc>

Differences from Regular Tooltip

Feature Tooltip (Tier 1/2) Card variant
Content model title or data-tooltip="id" [data-trigger] + [data-content]
Appearance Dark, compact, with arrow Light card surface, border, shadow, no arrow
Show delay 200ms 300ms
Hide delay 100ms 200ms
Popover type hint manual (supports interactive content)
ARIA role="tooltip", aria-describedby None (supplementary content)

tooltip-wc with Title Fallback

The <tooltip-wc> component also reads title from its trigger as a content source (lowest priority after <template data-tooltip> and data-content). This gives progressive enhancement to the web component path too.

<!-- Progressive enhancement: title provides no-JS fallback --> <tooltip-wc> <button type="button" title="Save your changes">Save</button> <template data-tooltip> <strong>Save document</strong><br> <kbd>Ctrl</kbd> + <kbd>S</kbd> </template> </tooltip-wc>

Content priority in tooltip-wc:

  1. <template data-tooltip> (rich HTML)
  2. data-content attribute (plain text, backward compatible)
  3. title on trigger (PE baseline — read and removed)

Pure HTML Tooltips — interestfor (Tier 0)

For simple tooltips that don't need arrows, custom positioning, or rich content, you can skip the init script entirely. The interestfor attribute (native in Chrome 133+, polyfilled in other browsers) handles hover/focus timing and popover show/hide declaratively.

The specification defines accessible rich internet applications.
Web Accessibility Initiative – Accessible Rich Internet Applications
<!-- Pure HTML tooltip — no init script needed --> <button interestfor="wai-def">WAI-ARIA</button> <div id="wai-def" popover="hint"> Web Accessibility Initiative – Accessible Rich Internet Applications </div>

Custom Timing

Control show/hide delays with CSS custom properties:

/* Custom timing via CSS custom properties */ #my-tooltip { --interest-delay-start: 300ms; --interest-delay-end: 150ms; }

When to Use Which

Use case Approach
Simple text hint on a button/link title + data-tooltip (Tier 1)
Rich content (kbd shortcuts, icons) data-tooltip="id" referencing a popover (Tier 2)
Interactive hover card (profiles, previews) <tooltip-wc data-variant="card"> (Tier 3)
Zero-JS inline definition interestfor + popover="hint" (Tier 0)

How It Works

Popover API

Tooltips use the native Popover API with popover="hint". This renders tooltips in the browser's top layer, avoiding z-index conflicts and overflow clipping issues.

CSS Anchor Positioning

In modern browsers (Chrome 125+, Safari 18+), tooltips use CSS Anchor Positioning for smooth, hardware-accelerated positioning. Older browsers fall back to JavaScript positioning automatically.

Browser Support

Browser Positioning Notes
Chrome 125+ CSS Anchor Full support
Safari 18+ CSS Anchor Full support
Firefox JS Fallback Anchor positioning not yet supported
Older browsers JS Fallback Requires Popover API support

Show Delay (tooltip-wc)

When using <tooltip-wc>, control how long to wait before showing with data-tooltip-delay. Default is 200ms.

<tooltip-wc data-tooltip-delay="0"> <button>Instant</button> <template data-tooltip>Shows immediately</template> </tooltip-wc> <tooltip-wc data-tooltip-delay="500"> <button>Delayed</button> <template data-tooltip>Waits 500ms</template> </tooltip-wc>

Events & API (tooltip-wc)

Events

Event Description
tooltip-show Fired when the tooltip becomes visible.
tooltip-hide Fired when the tooltip is hidden.
const tooltip = document.querySelector('tooltip-wc'); tooltip.addEventListener('tooltip-show', () => { console.log('Tooltip shown'); }); tooltip.addEventListener('tooltip-hide', () => { console.log('Tooltip hidden'); });

JavaScript API

Method/Property Type Description
show() method Show the tooltip immediately.
hide() method Hide the tooltip immediately.
isVisible property Read-only boolean indicating if the tooltip is currently visible.
const tooltip = document.querySelector('tooltip-wc'); // Show the tooltip tooltip.show(); // Check visibility console.log(tooltip.isVisible); // true // Hide the tooltip tooltip.hide();

Keyboard Support

Tooltips appear on keyboard focus and can be dismissed with the Escape key. Escape handling is built into the Popover API.

Key Action
Tab Focus the trigger element, showing the tooltip
Escape Hide the tooltip while keeping focus on the trigger

Use Tab to focus the button below, then press Escape to hide the tooltip:

Accessibility

ARIA Attributes

  • The tooltip has role="tooltip" and popover="hint"
  • The trigger gets aria-describedby pointing to the tooltip (set by init script)
  • The tooltip arrow has aria-hidden="true"
  • Uses the browser's top layer for proper stacking without z-index conflicts

Screen Reader Behavior

When the trigger receives focus, screen readers announce the element followed by the tooltip content (via aria-describedby). This provides context without requiring visual interaction.

Best Practices

  • Keep tooltip content brief and informative
  • Don't put essential information only in tooltips
  • Ensure the trigger element is focusable
  • Use tooltips for supplementary information, not primary content
  • Avoid interactive content inside tooltips (use cards instead)