Base components
Tooltip
A short dark label that names or explains the element under the pointer or in focus, which waits a moment, fades in with a small rise from its arrow side, and hides in about half the time.
Live preview
Loading preview
Icon buttons Copy link, Download and Delete with short tooltips on each side, a tooltip with supporting text, and a form label with a help icon. Replay opens the first tooltip, then moves to the next two buttons so the quick hand-off (TIP4) shows.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add tooltipUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { Button } from '../button/button';
import { HelpIcon, Tooltip, TooltipProvider } from './tooltip';
const STEP_MS = 900;
function LinkIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.67"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M8.333 10.833a4.167 4.167 0 0 0 6.284.45l2.5-2.5a4.167 4.167 0 0 0-5.892-5.891L9.792 4.317" />
<path d="M11.667 9.167a4.167 4.167 0 0 0-6.284-.45l-2.5 2.5a4.167 4.167 0 0 0 5.892 5.891l1.425-1.425" />
</svg>
);
}
function DownloadIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.67"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M10 2.5v10M5.833 8.333 10 12.5l4.167-4.167M3.333 15.833h13.334" />
</svg>
);
}
function TrashIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth="1.67"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2.5 5h15M7.5 5V3.333h5V5M15.833 5l-.6 10.2a1.667 1.667 0 0 1-1.666 1.633H6.433A1.667 1.667 0 0 1 4.767 15.2L4.167 5M8.333 8.333v5M11.667 8.333v5" />
</svg>
);
}
const ACTIONS = [
{ label: 'Copy link', side: 'top', icon: <LinkIcon /> },
{ label: 'Download', side: 'bottom', icon: <DownloadIcon /> },
{ label: 'Delete', side: 'right', icon: <TrashIcon /> },
] as const;
export default function TooltipDemo({ replay = 0 }: { replay?: number }) {
// Index of the tooltip Replay holds open; null leaves all of them to the pointer and keyboard.
const [active, setActive] = React.useState<number | null>(null);
React.useEffect(() => {
if (replay === 0) return;
const timers = [0, 1, 2, 3].map((step) =>
window.setTimeout(() => setActive(step < ACTIONS.length ? step : null), step * STEP_MS),
);
return () => {
for (const t of timers) window.clearTimeout(t);
};
}, [replay]);
return (
<TooltipProvider>
<div className="flex flex-col items-center gap-10">
<div className="flex items-center gap-3">
{ACTIONS.map((action, index) => (
<Tooltip
key={action.label}
content={action.label}
side={action.side}
open={active === null ? undefined : active === index}
>
<Button hierarchy="secondary" iconOnly aria-label={action.label}>
{action.icon}
</Button>
</Tooltip>
))}
<Tooltip
content="Share with your team"
supportingText="People with the link can view this page."
side="bottom"
>
<Button hierarchy="secondary">Share</Button>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<span className="text-sm font-medium text-secondary">Workspace name</span>
<HelpIcon
content="What is a workspace?"
supportingText="A workspace holds the projects and files of your team."
/>
</div>
</div>
</TooltipProvider>
);
}
API
Parts: trigger (the element it describes), popup (positioner and bubble), title, supporting text, arrow, help icon (HelpIcon): 16px icon button that opens the same bubble.
| Prop | Type | Default | Description |
|---|---|---|---|
| content | string | undefined | Title text, short, for example "Copy link". |
| supportingText | string | undefined | Second line with more detail. The bubble widens to 320px. |
| side | "top" | "bottom" | "left" | "right" | "top" | Side of the trigger the bubble shows on. It flips when there is no room. |
| align | "start" | "center" | "end" | "center" | Alignment to the trigger. |
| sideOffset | number | 8 | Gap between the trigger and the bubble, in px. |
| arrow | boolean | true | Shows the arrow that points at the trigger. |
| delay | number | 400 | Wait in ms after the pointer enters before the bubble opens. Focus opens at once. |
| closeDelay | number | 0 | Wait in ms after the pointer leaves before the bubble closes. |
| open | boolean | undefined | Controlled open state. |
| disabled | boolean | false | Never opens. |
| Event | Description |
|---|---|
| openChange | The bubble opened or closed. Payload: the new open state. |
| Slot | Description |
|---|---|
| default | The trigger element. |
| content | Rich title content, replaces the content prop. Text only: no links or buttons. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| TIP1 | pointer rests on the trigger (after delay), or keyboard focus (at once) | bubble | opacity | 0 → 1 | duration-sm (200)ease-outdelay 400ms | Shows at once |
| TIP2 | open (as TIP1) | bubble | translate from the trigger side | distance-rise toward the trigger → 0 | duration-sm (200)ease-outdelay 400ms | Shows at once |
| TIP3 | close (pointer leaves, blur, Escape or press) | bubble | opacity | 1 → 0 | duration-2xs (120)ease-exit | Hides at once |
| TIP4 | pointer moves to another trigger while a bubble is open, or within 300ms after one closed | new bubble | opacity | 0 → 1 (no rise, no delay) | duration-2xs (120)ease-out | Shows at once |
| TIP5 | hover the help icon | help icon | color | fg-tertiary → fg-tertiary_hover | duration-sm (200)ease-out | Swaps at once |
| TIP6 | keyboard focus on the help icon | help icon hit area | box-shadow | none → focus ring (2px bg-primary gap, 4px focus-ring) | duration-2xs (120)ease-out | Shows at once |
Accessibility
- Built on a tooltip primitive: the bubble has role tooltip and is linked to the trigger with aria-describedby (or names an icon-only trigger with aria-labelledby when it is its only label).
- Opens on hover after delay and at once on keyboard focus; Escape closes it without moving focus; the pointer can move onto the bubble without closing it (WCAG 1.4.13).
- A tooltip never holds the only copy of needed information and never holds links or buttons; use a popover or the help icon for longer help.
- Touch screens do not show tooltips; anything a touch user needs goes in a visible label or the help icon.
- HelpIcon is a native button named "More information" that opens on hover, focus and tap (a popover that opens on hover), so touch and keyboard users can read it; it has a 24 x 24px tap area around the 16px icon (WCAG 2.5.8).
- Reduced motion: the bubble shows and hides at once; the delay stays, because it prevents bubbles from flashing while the pointer passes.