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.
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
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.
npm install @eunomia/elements
<script type="module">
import "@eunomia/elements/tooltip.js";
</script>
<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>
npm install @eunomia/elements
import "@eunomia/elements/tooltip.js";
function DeleteButton() {
return (
<eun-tooltip heading="Delete this item">
<eun-button slot="anchor" rounded aria-label="Delete">
<eun-icon name="delete" size="18"></eun-icon>
</eun-button>
</eun-tooltip>
);
}
npm install @eunomia/elements
"use client";
import "@eunomia/elements/tooltip.js";
export function DeleteButton() {
return (
<eun-tooltip heading="Delete this item">
<eun-button slot="anchor" appearance="ghost" rounded aria-label="Delete">
<eun-icon name="delete" size="18"></eun-icon>
</eun-button>
</eun-tooltip>
);
}
npm install @eunomia/elements
<script setup>
import "@eunomia/elements/tooltip.js";
</script>
<template>
<eun-tooltip heading="Delete this item">
<eun-button slot="anchor" appearance="ghost" rounded aria-label="Delete">
<eun-icon name="delete" size="18"></eun-icon>
</eun-button>
</eun-tooltip>
</template>
npm install @eunomia/elements
import { CUSTOM_ELEMENTS_SCHEMA, Component } from "@angular/core";
import "@eunomia/elements/tooltip.js";
@Component({
selector: "app-delete-button",
template: `
<eun-tooltip heading="Delete this item">
<eun-button slot="anchor" appearance="ghost" rounded aria-label="Delete">
<eun-icon name="delete" size="18"></eun-icon>
</eun-button>
</eun-tooltip>
`,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class DeleteButtonComponent {}
Alternatives
eun-toastGuidance
- Keep the content to a short label or a single sentence: a tooltip is glanced at, not read
- Make sure the
anchorelement is itself focusable (a realbutton/a, or an explicittabindex="0"): a tooltip that only shows on hover is invisible to keyboard users - Use
lazyon pages with many tooltips where few are ever actually opened - Reserve
severityfor 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
| Name | Type | Default | Description |
|---|---|---|---|
| rich-text | string | — | Lightweight formatted content rendered above the description slot, combining with it. Supports simple lists, links, bold, and italic text |
| lazy | boolean | false | Defers 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-delay | number | 100 | Milliseconds 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-delay | number | 150 | Milliseconds 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 |
| heading | string | — | The tooltip's heading, as a property. When unset, the default slot's content is used instead |
| open | boolean | false | Whether 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
Slots
| Name | Description |
|---|---|
| anchor | The element the tooltip is attached to. Required |
| (default) | The tooltip's heading text. Ignored once the heading property is set |
| description | Extra detail below the heading, combining with richText if both are set |
CSS custom properties
| Name | Description |
|---|---|
| --tooltip-z-index | Sets the stacking order of the tooltip |
| --tooltip-gap | Sets the distance between the anchor and the panel |
| --tooltip-viewport-gap | Sets the minimum distance kept from the viewport edge, so the panel never runs off-screen |
| --tooltip-min-width | Sets the minimum width of the panel |
| --tooltip-max-width | Sets the maximum width of the panel |
| --tooltip-background-color | Sets the background color when severity is unset |
| --tooltip-color | Sets the heading color when severity is unset |
| --tooltip-content-color | Sets the color of the description content |
| --tooltip-font-weight | Sets the font weight of the description content, not the heading, and not richText, which stays at its own normal weight |
| --tooltip-border-radius | Sets the corner radius of the panel |
| --tooltip-padding | Sets the padding of the panel |
| --tooltip-shadow | Sets the shadow of the panel |
| --tooltip-transition-duration | Sets 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).
<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).
<!-- 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.
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.
<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
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.
<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.
<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.
<!-- 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 - 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.
<!-- 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.
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.
<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.
<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
Content on Hover or Focus (WCAG 1.4.13)
- Dismissible.
Escapecloses the tooltip without moving focus away from the anchor, at any time. - 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. - 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-toastdoes.
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
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).
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
Reference links
tooltip role