Eunomia v1.0.0-beta.2
AllAngularReactNext.jsJavaScriptVue
EunomiaSalmonForestVioletOceanGoldFireCustom…
🇬🇧 English🇫🇷 Français

Tooltip

Tooltip is a floating, non-interactive description anchored to whatever element it's attached to. It's positioned with the CSS Anchor Positioning API where the browser supports it, and a JS fallback everywhere else. Either way, the panel is guaranteed to never render past the viewport edge, clamping back on-screen even in a spot the browser's own single-axis flip doesn't cover on its own.

Overview API Examples Accessibility

This element will be visible from your personal space

When to use

Reach for a tooltip to label or add a short hint to a control whose purpose isn't already obvious from its own visible text, such as an icon-only button, an abbreviation, or a field that needs a one-line clarification. It's meant to be glanced at, not read carefully. Keep the content short, and never put anything in it a person genuinely needs to complete a task, since a tooltip is easy to miss entirely on a touch device with no hover. If the message needs to stay on screen without a hover, focus, or click gesture keeping it there, or reports something that just happened, use toast instead. If the content is itself interactive, such as a form or a list of actions, a tooltip is the wrong shape entirely, see "No interactive content" in the Accessibility tab.

Install & usage

Pick a framework in the toolbar above and these snippets adapt. Unlike toast, a tooltip is anchored to a specific element already in the page's own layout. Slot that element into anchor rather than appending the tooltip itself elsewhere.

npm install @eunomia/elements
import "@eunomia/elements/tooltip.js";
<eun-tooltip heading="Add to favorite" placement="top">
  <p slot="description">
    This element will be visible from your personal space
  </p>
  <eun-button slot="anchor" rounded aria-label="Delete">
    <eun-icon name="star" size="18"></eun-icon>
  </eun-button>
</eun-tooltip>

Importing the file registers <eun-tooltip> as a custom element, with no further setup needed. It works with any framework, or none, since it's a standard web component.

Alternatives

You want .. Prefers A message that persists without a hover, focus, or click gesture keeping it open, or that reports something that just happened eun-toast

Guidance

  • Keep the content to a short label or a single sentence: a tooltip is glanced at, not read
  • Make sure the anchor element is itself focusable (a real button/a, or an explicit tabindex="0"): a tooltip that only shows on hover is invisible to keyboard users
  • Use lazy on pages with many tooltips where few are ever actually opened
  • Reserve severity for a genuine info/success/warning/critical/neutral distinction, not as a decoration
  • Putting anything a person must read to complete a task inside a tooltip: it's too easy to miss, especially on touch
  • Slotting interactive content (a button, a link, a form field): see "No interactive content" in the Accessibility tab
  • Using a tooltip for a message that should persist without a hover/focus/click gesture keeping it open: see Alternatives above

Live testing

Properties

Tooltip <eun-tooltip>

Attributes

NameTypeDefaultDescription
rich-textstringLightweight formatted content rendered above the description slot, combining with it. Supports simple lists, links, bold, and italic text
lazybooleanfalseDefers creating the panel's DOM until the tooltip is shown for the first time, instead of at page load. Set this when many tooltips exist on a page but few are ever actually opened
show-delaynumber100Milliseconds to wait before showing on hover. Has no effect on focus, which shows immediately, or on the show method, open, or click to pin
hide-delaynumber150Milliseconds to wait before hiding once the pointer leaves the anchor, long enough to move it into the panel itself without closing
severity'info' | 'success' | 'warning' | 'critical' | 'neutral'The severity of the tooltip. Drives the heading accent color, taking priority over color when both are set
color'primary' | 'secondary' | 'light' | 'medium' | 'dark''primary'A plain brand or neutral color, used only while severity is unset
appearance'default' | 'flat' | 'fill' | 'outline''default'The visual style of the panel's surface, using the same names as the alert component, though always fully opaque rather than a translucent wash. Default is a pale, opaque tint of color or severity with accent colored text and no border. Setting color to dark renders it as a solid near-black surface with light text instead of a pale one
placement'top' | 'right' | 'bottom' | 'left''top'The preferred side to open on, flipping to the opposite side automatically when there isn't room
headingstringThe tooltip's heading, as a property. When unset, the default slot's content is used instead
openbooleanfalseWhether the panel is currently shown. Also settable from outside through the show, hide, and toggle methods

Import the exact TypeScript type behind any property above, see Types.

Slots

NameDescription
anchorThe element the tooltip is attached to. Required
(default)The tooltip's heading text. Ignored once the heading property is set
descriptionExtra detail below the heading, combining with richText if both are set

CSS custom properties

NameDescription
--tooltip-z-indexSets the stacking order of the tooltip
--tooltip-gapSets the distance between the anchor and the panel
--tooltip-viewport-gapSets the minimum distance kept from the viewport edge, so the panel never runs off-screen
--tooltip-min-widthSets the minimum width of the panel
--tooltip-max-widthSets the maximum width of the panel
--tooltip-background-colorSets the background color when severity is unset
--tooltip-colorSets the heading color when severity is unset
--tooltip-content-colorSets the color of the description content
--tooltip-font-weightSets the font weight of the description content, not the heading, and not richText, which stays at its own normal weight
--tooltip-border-radiusSets the corner radius of the panel
--tooltip-paddingSets the padding of the panel
--tooltip-shadowSets the shadow of the panel
--tooltip-transition-durationSets the duration of the open and close transition

Placement

placement picks the preferred side: top (default), right, bottom, or left. It flips to the opposite side automatically when there isn't enough room, and clamps to the viewport edge rather than ever running off-screen. For an anchor sitting right in a corner, both axes clamp at once (see "Never off-screen" below).

Opens above the anchor. Top Opens to the right of the anchor. Right Opens below the anchor. Bottom Opens to the left of the anchor. Left
<eun-tooltip placement="right" heading='placement="right"'>
  <span slot="description">Opens to the right of the anchor.</span>
  <eun-button slot="anchor">Right</eun-button>
</eun-tooltip>

Anchoring

anchor accepts anything: an icon-only button, a full-width button, or a plain piece of text. The only requirement is that it can receive focus. eun-button already can, but plain text needs an explicit tabindex="0" added, or a keyboard user can never trigger the tooltip at all (see "Anchor must be focusable" in the Accessibility tab).

Manage your account and preferences. No confirmation step needed. Save Billed Charged only for the days left in the current billing cycle. pro rata
<!-- Icon-only button : give it its own aria-label, the tooltip doesn't provide one -->
<eun-tooltip heading="Settings">
  <span slot="description">Manage your account and preferences.</span>
  <eun-button slot="anchor" appearance="ghost" rounded aria-label="Settings">
    <eun-icon name="settings" size="18"></eun-icon>
  </eun-button>
</eun-tooltip>

<!-- Plain text : tabindex="0" is required for keyboard/focus access -->
<eun-tooltip heading="Pro rata">
  <span slot="description"
    >Charged only for the days left in the current billing cycle.</span
  >
  <span
    slot="anchor"
    tabindex="0"
    style="border-bottom: 1px dotted currentColor; cursor: help;"
  >
    pro rata
  </span>
</eun-tooltip>

Content

A tooltip can carry just a heading, just a description slot, or both together. A heading alone reads as a short label, a description alone reads as a plain sentence with no bold emphasis, and together, the heading acts as a short title with the description adding the supporting detail underneath it, the same combination already shown in the Overview tab's "Add to favorite" example, kept here for comparison against the other two.

Heading only Description only, with no heading above it. Description only

This element will be visible from your personal space

<!-- Heading only -->
<eun-tooltip heading="Heading only">
  <eun-button slot="anchor">Heading only</eun-button>
</eun-tooltip>

<!-- Description only -->
<eun-tooltip>
  <span slot="description">Description only, with no heading above it.</span>
  <eun-button slot="anchor">Description only</eun-button>
</eun-tooltip>

<!-- Both — the same example already shown in the Overview tab -->
<eun-tooltip heading="Add to favorite">
  <p slot="description">
    This element will be visible from your personal space
  </p>
  <eun-button slot="anchor" rounded aria-label="Add to favorite">
    <eun-icon name="star" size="18"></eun-icon>
  </eun-button>
</eun-tooltip>

Severity

Five severities are available, each pairing an accent color for the heading with a pale, always-opaque tint of that same color for the background (see "Types" below for the full picture, never a translucent wash). Each eun-tag below carries the same severity as the tooltip it opens, so the tag's own color previews which severity is about to show.

Try it from Settings > Beta features. Info Your profile has been updated. Success Upgrade to keep access to Pro features. Warning Update your payment method to continue. Critical The service will be briefly unavailable Sunday at 2am. Neutral
<eun-tooltip severity="warning" heading="Expires in 3 days">
  <span slot="description">Upgrade to keep access to Pro features.</span>
  <eun-tag slot="anchor" severity="warning" tabindex="0">Warning</eun-tag>
</eun-tooltip>

Part of the shared vocabulary covered in Variants, alongside every other component that reuses it.

Color

color is a plain brand/neutral palette pick: primary (the default), secondary, light, medium, or dark, for a tooltip that isn't tied to any particular severity. Same vocabulary and precedence as eun-tag's own color. It's only ever used while severity is unset, which takes over entirely the moment it's set. dark renders differently from the other four: instead of the usual pale tint, it's a solid, near-black surface with light text, since a "pale dark" would just read as a plain light grey and defeat the point of picking it in the first place.

The default brand color, no severity attached. Primary A secondary brand accent. Secondary A subtle, low-emphasis neutral. Light A stronger, mid-weight neutral. Medium Solid near-black, with light text. Dark
<eun-tooltip color="secondary" heading="Secondary">
  <span slot="description">A secondary brand accent.</span>
  <eun-tag slot="anchor" color="secondary" tabindex="0">Secondary</eun-tag>
</eun-tooltip>

Appearances

appearance picks the surface treatment, matching eun-alert's own vocabulary: default (the default: a pale, opaque tint of color/severity, accent-colored text, no border), flat (a plain neutral surface, unaffected by color/severity), outline (the same neutral surface, colored border), or fill (a solid color/severity-colored background, light text). Unlike eun-alert's own default/flat, every one of these stays fully opaque here, never a translucent wash, since a tooltip has no control over what it floats above. See "Color" above for dark's own exception to default's usual pale treatment.

Pale tint, accent text, no border. Default Plain neutral surface, no accent. Flat Neutral surface, colored border. Outline Solid color, light text. Fill
<eun-tooltip appearance="fill" severity="info" heading="Fill">
  <span slot="description">Solid color, light text.</span>
  <eun-button slot="anchor">Fill</eun-button>
</eun-tooltip>

Never off-screen

Native position-try-fallbacks only ever flips a single axis, enough for an anchor near the top/bottom edge while placement is "top"/ "bottom", but not for one also close to a side edge at the same time. eun-tooltip corrects for that itself on every reposition, on top of whichever positioning path placed the panel. The button below is pinned (position: fixed) to your actual browser window's bottom-right corner for this example, not just a demo box standing in for it. Hover it: placement="right" would normally push most of the panel past the right edge of the screen, but it clamps back fully visible instead.

A "?" button is pinned to your window's real bottom-right corner for this one example, hover it.
Clamped back on-screen, never cut off.
<!-- An anchor pinned to a real screen corner, placement="right" would
     normally push the panel mostly off the right edge — eun-tooltip
     clamps it back on-screen automatically, no extra configuration needed. -->
<eun-tooltip placement="right" heading="Still fully visible">
  <span slot="description">Clamped back on-screen, never cut off.</span>
  <eun-button slot="anchor" style="position: fixed; bottom: 16px; right: 16px;">
    Corner anchor
  </eun-button>
</eun-tooltip>

Rich text

richText accepts the same markdown-lite engine as eun-alert/eun-toast. See Markdown-lite for the full syntax: - item lists, [label](url) links, **bold**, *italic*. It renders above the description slot, combining with it if both are set, useful for a short list of bullet points, or a link to more detail, without needing real markup slotted in. Keep it short regardless: a tooltip is still glanced at, not read carefully, even with richer formatting available.

Adds a one-time code to your sign-in.
<!-- richText renders above the description slot, combining with it. -->
<eun-tooltip
  severity="info"
  heading="Two-factor authentication"
  rich-text="**Recommended** for every account, see [how it works](/guides/) for details."
>
  <span slot="description">Adds a one-time code to your sign-in.</span>
  <eun-button slot="anchor" rounded aria-label="More info">
    <eun-icon name="info" size="18"></eun-icon>
  </eun-button>
</eun-tooltip>
document.querySelector("#info-tooltip").richText =
  "Adds a one-time code to your sign-in. **Recommended** for every account.";

Lazy loading

By default, a tooltip's panel is part of the DOM from the moment eun-tooltip itself renders, cheap for one or two tooltips, but wasteful on a page with dozens where most are never opened. Set lazy and the panel simply doesn't exist in the DOM until the tooltip is shown for the first time (hover, focus, or a click on the anchor). Closing it again afterward never tears it back down, so only the first open pays that cost.

The panel itself isn't rendered inside eun-tooltip's own shadow root. It's created directly alongside the anchor instead, so CSS Anchor Positioning can actually resolve it (shadow DOM intentionally hides an anchor-name from outside its own tree), so there's no shadowRoot to peek into to prove this from outside. The status line below instead watches the anchor's own aria-describedby: eun-tooltip only ever sets it once the panel has actually been created, which happens on that very first open. Hover or focus the button and watch it flip.

Created on first hover, not on page load. Hover me (lazy)

Panel created: false

<eun-tooltip lazy heading="Now in the DOM">
  <span slot="description">Created on first hover, not on page load.</span>
  <eun-button slot="anchor">Hover me (lazy)</eun-button>
</eun-tooltip>
// Before the first hover/focus/click, eun-tooltip hasn't linked the panel
// to the anchor yet, since it doesn't exist :
anchor.hasAttribute("aria-describedby"); // false

// After it, the panel exists for good — later closes don't remove it again.

Toggle button control

open is a regular reflected property, and show()/hide()/toggle() are public methods, so any control, not just the anchor itself, can drive a tooltip open and closed. Useful on touch, where hover doesn't exist at all.

No action needed, billed automatically. Pro plan Toggle tooltip
<eun-tooltip id="plan-tooltip" heading="Your plan renews on the 12th">
  <span slot="description">No action needed, billed automatically.</span>
  <eun-tag slot="anchor" tabindex="0">Pro plan</eun-tag>
</eun-tooltip>
<eun-button id="toggle-button">Toggle tooltip</eun-button>
const tooltip = document.querySelector("#plan-tooltip");
document.querySelector("#toggle-button").addEventListener("click", () => {
  tooltip.toggle(); // same as: tooltip.open = !tooltip.open
});

Custom theme

Every color is a CSS variable, read only while severity is unset. Set severity and these are ignored in favor of the built-in palette. --tooltip-color sets the heading, --tooltip-content-color sets the description independently, and --tooltip-background-color overrides the (always opaque) background.

Beyond color, three defaults are deliberate design choices rather than arbitrary values, and every one of them stays a plain CSS variable to override if a given use case needs to. The description renders a touch lighter than the heading (--eun-fontWeight-s, 300, via --tooltip-font-weight, applied to the description only, since the heading stays bold, and richText keeps its own normal weight regardless, so its **bold**/plain-paragraph distinction stays meaningful). The corners are a little more rounded than most compact components (--eun-borderRadius-m, 10px, via --tooltip-border-radius), and the shadow is the subtlest step available (--eun-shadow-xs, via --tooltip-shadow), since a tooltip is a light touch, not a heavy floating card.

Acme Inc. invited you to join their workspace. Invite
<eun-tooltip
  heading="Workspace invite"
  style="
    --tooltip-background-color: #f4effa;
    --tooltip-color: #4a2e83;
    --tooltip-font-weight: 500;
    --tooltip-border-radius: 4px;
    --tooltip-shadow: var(--eun-shadow-m);
    --tooltip-content-color: #6c4ab0;
  "
>
  <span slot="description">Acme Inc. invited you to join their workspace.</span>
  <eun-button slot="anchor">Invite</eun-button>
</eun-tooltip>

Keyboard interactions

Key Action
Tab / Shift+Tab Moves focus to/from the anchor element, showing/hiding the tooltip along the way
Escape Dismisses the tooltip, regardless of how it was opened (hover, focus, or a click-pin)

eun-tooltip itself carries no tabindex and is never a tab stop on its own. Only the anchor element (and whatever else is on the page) is.

Anchor must be focusable

The tooltip shows on focusin/hides on focusout of whatever's slotted into anchor, in addition to hover. This is what makes it usable without a mouse at all. eun-button, eun-tag, and native interactive elements (button, a[href], form fields) are already focusable. Plain content (a span, an abbr) is not, and needs an explicit tabindex="0" added, see "Anchoring an icon, a button, or plain text" in the Examples tab. Without it, the tooltip's content is only ever reachable by mouse hover, which fails WCAG 2.1.1 Keyboard.

Content on Hover or Focus (WCAG 1.4.13)

WCAG 1.4.13 is the guideline this component is built around, satisfied three ways:

  1. Dismissible. Escape closes the tooltip without moving focus away from the anchor, at any time.
  2. Hoverable. hideDelay (default 150ms) is the time kept between the pointer leaving the anchor and the tooltip actually closing, long enough to move the pointer from the anchor onto the panel itself (moving over the panel cancels the pending close outright) without it disappearing first.
  3. Persistent. Once shown, the tooltip stays open for as long as the anchor is hovered or focused (or pinned by a click, see "Toggle button control" in the Examples tab). It never times out or disappears on its own the way eun-toast does.

showDelay (default 100ms) is a separate, smaller nicety on top: it keeps a tooltip from flashing open every time the pointer merely passes over the anchor on its way elsewhere. It only applies to hover. Focusing the anchor with the keyboard always shows it immediately, since that's already a deliberate act with no "passing through" equivalent.

No interactive content

The panel deliberately has no slot for interactive content (no action button, no link) and renders role="tooltip". The WAI-ARIA Tooltip pattern explicitly assumes non-interactive content, since a screen reader user has no reliable way to Tab into a tooltip's own content in the first place. If what you need is closer to a small interactive panel (a button, a form), reach for a different pattern entirely rather than stretching a tooltip past what role="tooltip" can honestly represent.

aria-describedby links the panel to the anchor

The first time a tooltip is shown, eun-tooltip sets a plain aria-describedby="<id>" on the anchor element pointing at the panel, an ordinary, reliable ARIA reference, so a screen reader announces the panel's content as a description of the anchor. This works cleanly because the panel is deliberately not rendered inside eun-tooltip's own shadow root (see "Lazy loading" above): it's created directly alongside the anchor instead, in the same part of the DOM, rather than behind a shadow boundary a plain id reference can't cross. Still, never put anything in a tooltip a person must have read to proceed (see "When to use" in the Overview tab). A tooltip is supplementary by nature, not guaranteed to be noticed.

Don't rely on color alone

eun-tooltip never renders an icon of its own, deliberately. A tooltip communicates through its text, not a decoration next to it (an icon-only anchor is a different matter, and needs its own aria-label regardless, see "Anchoring an icon, a button, or plain text" above). WCAG 1.4.1 Use of Color requires that information conveyed by color also be available another way. State the nature of the message in the heading text itself, not just a colored accent.

Color contrast

Every built-in severity combination reuses this design system's own color tokens, already chosen to meet contrast minimums against the tooltip's opaque background. A custom --tooltip-background-color/--tooltip-color/--tooltip-content-color combination (severity unset, see "Custom colors" in the Examples tab) must still be checked against WCAG 1.4.3 Contrast (Minimum). See Color for the tokens that already meet it.

Reference links

WCAG 1.4.13: Content on Hover or Focus
WCAG 2.1.1: Keyboard
WCAG 1.4.1: Use of Color
WCAG 1.4.3: Contrast (Minimum)
WAI-ARIA APG: Tooltip Pattern
WAI-ARIA: tooltip role