Lunia DS

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.

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 tooltip

Usage

The demo above, as code.

demo.tsx
'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.

PropTypeDefaultDescription
contentstringundefinedTitle text, short, for example "Copy link".
supportingTextstringundefinedSecond 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.
sideOffsetnumber8Gap between the trigger and the bubble, in px.
arrowbooleantrueShows the arrow that points at the trigger.
delaynumber400Wait in ms after the pointer enters before the bubble opens. Focus opens at once.
closeDelaynumber0Wait in ms after the pointer leaves before the bubble closes.
openbooleanundefinedControlled open state.
disabledbooleanfalseNever opens.
EventDescription
openChangeThe bubble opened or closed. Payload: the new open state.
SlotDescription
defaultThe trigger element.
contentRich 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
TIP1pointer rests on the trigger (after delay), or keyboard focus (at once)bubbleopacity0 → 1duration-sm (200)ease-outdelay 400msShows at once
TIP2open (as TIP1)bubbletranslate from the trigger sidedistance-rise toward the trigger → 0duration-sm (200)ease-outdelay 400msShows at once
TIP3close (pointer leaves, blur, Escape or press)bubbleopacity1 → 0duration-2xs (120)ease-exitHides at once
TIP4pointer moves to another trigger while a bubble is open, or within 300ms after one closednew bubbleopacity0 → 1 (no rise, no delay)duration-2xs (120)ease-outShows at once
TIP5hover the help iconhelp iconcolorfg-tertiary → fg-tertiary_hoverduration-sm (200)ease-outSwaps at once
TIP6keyboard focus on the help iconhelp icon hit areabox-shadownone → focus ring (2px bg-primary gap, 4px focus-ring)duration-2xs (120)ease-outShows 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.