Skip to main content

Tooltip

Implemented

Hover/focus tooltip portaled to document.body as a <div role='tooltip'> (role='dialog' when interactive), linked to its single trigger via aria-describedby. No show/hide delay exists and the portaled surface is positioned against the viewport, not the anchor — surface positioning is being overhauled (tracked).

Categorymolecules
Sourcepatterns/molecules/tooltip.html
AI HintIcon-only buttons and abbreviations. Content is consumer-provided — i18n belongs to the caller. Controlled mode via open + onOpenChange. Escape dismissal and show/hide delays are not implemented yet (WCAG 1.4.13 gap, tracked).

Tier-2 molecule alias for Tooltip. Same component as the base Tooltip; the long-form alias keeps the *Molecule-family naming consistent.

TooltipMolecule

Overlay molecule — Family 6 (Tier 2 alias)

Floating surface that surfaces contextual text on hover or focus. Clones the child to attach hover/focus/blur listeners; portals to document.body to escape parent overflow contexts.

This component supports light and dark mode.

Usage

TooltipMolecule accepts one anchor child (a single React element). It attaches the hover/focus listeners via React.cloneElement — anchor components that already define onMouseEnter/onFocus are preserved.

import { Tooltip as TooltipMolecule, Button } from '@epasslive/design-system'

<TooltipMolecule content="Saves the event." placement="right">
  <Button variant="primary">Save</Button>
</TooltipMolecule>

Placement — known limitation

placement is a static hint ("top" | "right" | "bottom" | "left", default "top"). There is no arrow and no collision logic. Be aware: the surface portals to document.body with position: fixed while using viewport-relative utility classes, so the percentage offsets resolve against the viewport rather than the trigger — the bubble currently renders off-screen in every placement and the demos above stay silent on hover. This is a tracked defect (positioning overhaul: anchor measurement via getBoundingClientRect + viewport clamping — see cascade step 6); no show/hide delay exists either, despite older docs claiming 200ms.

Props

Prop
Type
Default
Description
content*
React.ReactNode
—

Body of the tooltip surface.

children*
React.ReactElement
—

A single React element used as the anchor.

placement
TooltipPlacement
—

"top" | "right" | "bottom" | "left". Defaults to "top".

open
boolean
—

Controlled visibility. When omitted, hover/focus drive the internal state.

onOpenChange
(open: boolean) => void
—

Called on every hover/focus state change. Pair with `open` for the controlled (Popover-style) contract where the parent owns the open state.

interactive
boolean
—

When true the floating content receives pointer events (links/controls inside stay clickable). Default false keeps the bubble pointer-events-none. Use controlled mode (`open` + `onOpenChange`) with interactive content so the pointer can move from trigger to bubble without closing it.

ariaLabel
string
—

Accessible name forwarded to the tooltip surface.

id
string
—

Optional id for the surface; auto-generated when omitted.

Props

Preview

Tooltip

Prop
Type
Required
Default
Description
content
React.ReactNode
Required
—

Tooltip text/content; i18n belongs to the consumer.

children
React.ReactElement
Required
—

Single trigger element — exactly one child (its props are cloned to attach aria-describedby).

placement
'top' | 'right' | 'bottom' | 'left'
Optional
'top'

Positioning preset — currently a static hint; the surface is not measured against the trigger (tracked fix).

open
boolean
Optional
uncontrolled

Controlled visibility; omit for the internal hover/focus state (pairs with onOpenChange).

onOpenChange
(open: boolean) => void
Optional
—

Notified on hover/focus change; pairs with controlled `open` for the parent-owns-state contract (Popover replacement, D1 merge 3).

interactive
boolean
Optional
false

Floating content receives pointer events so links/controls inside stay clickable; the surface then renders role='dialog'.

ariaLabel
string
Optional
—

Accessible name for the interactive surface (used when interactive=true).

id
string
Optional
auto (useId)

Optional id for the surface; auto-generated when omitted — the trigger's aria-describedby points at it.

Quick use:<Tooltip />

Accessibility

ARIA roletooltip (dialog when interactive)

Type

1 rule

NASA Rule 9

children must be exactly one element (NASA Rule 9 — composition over drilling)

ARIA

1 rule

WCAG 1.3.1

Trigger must get aria-describedby pointing to tooltip id; surface is role='tooltip' (role='dialog' when interactive)

Usage

Import

tsxSnippet
import { Tooltip } from "@epasslive/design-system"

Example 1

tsxSnippet
<Tooltip content="Save the event"><Button>Save</Button></Tooltip>

Example 2

tsxSnippet
<Tooltip content={help} open={open} onOpenChange={setOpen} interactive><a href="/help">Help</a></Tooltip>

Anti-patterns

— Do not put critical actions inside a non-interactive tooltip — content is pointer-events-none unless interactive

— Do not assume anchor-aware positioning — the portaled surface is currently viewport-positioned (tracked overhaul)

Design System v4.1.0 | ePassLive