Hover the chip
Overview
Tooltips are short, transient. They appear on hover or focus, they carry text only, they never replace a click target. Reach for one when an icon needs a name, a date wants a longer format, or a truncated label deserves to be readable on demand. If the content is essential to the task, write it into the page instead — a tooltip is supplementary.
Hover-only is not enough
Touch users can't hover; keyboard users can't mouse. The component opens on both hover and focus so every input modality reaches the content. Mobile relies on focus alone.
Variants
Two color treatments. Default is light (white surface, hairline border) — best on dark or busy contexts. Inverse is charcoal — the everyday choice on the light surfaces most of the product lives on.
variant="default"
Light surface, charcoal text, 1 px hairline border. Use on dark or busy contexts where an inverse pill would shout.
variant="inverse"
Charcoal surface, white text. Default everywhere — reads cleanly on the light pages most of the product lives on.
Placements
Top, right, bottom, left. The arrow tracks to the trigger; the component auto-flips to the opposite side if the preferred placement would overflow the viewport.
Four placements
Hover each anchor to see the arrow track to the trigger. The component auto-flips to the opposite edge if it would overflow the viewport.
Compositions
Three composition shapes. Single-line is the default; a label + description pair handles the “Settled · 16 Jan, 19:27” look from the Figma matrix; arrowless pills slot into dense rows where neighbouring cells shouldn't see a triangle.
Single line
Pass content for a one-line tooltip. Default size — Inter Tight Regular 14 / 20.
Title + description
Use label + description to mirror the Figma "Label / 16 Jan, 19:27" pattern — Inter Tight Semibold 16 / 24 over Regular 14 / 20.
No arrow
Pass showArrow={false} for an arrowless pill — used in dense rows where the arrow would clutter neighbouring cells.
Auto-flip
Hover this one — placed against the edge so the tooltip flips to the opposite side instead of overflowing.
Anatomy
Four named parts. Only the surface is required — every other piece is opt-in.
Surface
8 px radius. Inverse: #000000 charcoal fill, no border. Default: #FFFFFF with a 1 px #EEEEEF hairline.
Label
Optional title — Inter Tight Semibold 16 / 24. Use sparingly; most tooltips don't need a heading.
Body
Inter Tight Regular 14 / 20. Pass via content for a single line, or description for the line under a label.
Arrow
8 px rotated square pointing at the trigger. Auto-positions per placement. Toggle via showArrow.
Behavior
- Hover delay, focus instant.
delayMsdefaults to 400 ms — long enough to skip accidental hovers. Focus opens the tooltip immediately so keyboard users don't wait. - Auto-flip at the viewport edge. The component measures itself, the trigger, and the viewport. If the preferred placement overflows, it tries the opposite. No third-party positioning library — just
getBoundingClientRectand a small computation. - Closes on Escape, scroll, resize. Escape always closes. Scroll and resize close so the tooltip never hangs in a stale position. The user re-hovers to bring it back.
- Pointer events pass through. The tooltip sets
pointer-events: none— the cursor moves over it without triggering a hover-leave on the trigger. - Portal rendering. The tooltip mounts into
document.bodyso it can't be clipped by an ancestor'soverflow: hidden. SSR-safe — only mounts after the client hydrates.
Accessibility
role="tooltip". The tooltip surface carries the role; the trigger getsaria-describedbypointing at it while open. Screen readers announce the tooltip text after the trigger's name.- Keyboard parity. The trigger fires
onFocus, not justonMouseEnter— Tab into a tooltipped control and the content shows immediately. - Not for required information.A tooltip is supplementary. If the user can't complete the task without reading it, write it into the page — keyboard users on Windows High Contrast, screen-reader users with hovers off, and any user on a touch device may never see it.
- Don't place interactive content inside. The tooltip is
pointer-events: none— buttons or links inside can't be clicked. For interactive content, reach forPopover.
Code
tsx
import { Tooltip } from "@flatpay-dk/ui";
// Default — single-line content, top placement
<Tooltip content="Settled at end of day">
<Button>Pay out</Button>
</Tooltip>
// Inverse (charcoal) — the everyday default
<Tooltip content="Cancel the run" variant="inverse">
<IconButton aria-label="Cancel"><CrossIcon /></IconButton>
</Tooltip>
// Title + description (Figma "Label / 16 Jan, 19:27" pattern)
<Tooltip label="Settled" description="16 Jan, 19:27 · final">
<span className="border-b border-dashed">settled</span>
</Tooltip>
// Placement override
<Tooltip content="More info" placement="right">
<InfoIcon />
</Tooltip>
// No arrow — for dense rows
<Tooltip content="Run prototype locally" showArrow={false}>
<Button size="sm">Run</Button>
</Tooltip>
// Custom delay
<Tooltip content="Quick reveal" delayMs={120}>
…
</Tooltip>
// Disabled — never opens
<Tooltip content="…" disabled={!showHints}>
<Button>…</Button>
</Tooltip>Best practices
Do
Use tooltips for short, supplementary context — the icon's name, the date format behind a relative time.
Don't
Don't put essential information in a tooltip. Touch users and assistive tech may never see it.
Do
Pair tooltips with icon-only buttons — the tooltip is the button's accessible name in a visible form.
Don't
Don't put interactive controls inside a tooltip — pointer-events are off, the user can't click them.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| content | ReactNode | — | Single-line tooltip content. Mutually exclusive with label + description. |
| label | ReactNode | — | Title — Inter Tight Semibold 16 / 24. Pair with description for two-line tooltips. |
| description | ReactNode | — | Body — Inter Tight Regular 14 / 20. |
| placement | "top" | "right" | "bottom" | "left" | "top" | Preferred placement; auto-flips to the opposite side if it would overflow the viewport. |
| variant | "default" | "inverse" | "default" | default = light (white + hairline); inverse = charcoal. Reach for inverse on light pages. |
| delayMs | number | 400 | Delay before opening on hover. Doesn't apply to focus — keyboard users get instant open. |
| showArrow | boolean | true | Show the 8 px arrow pointing at the trigger. |
| disabled | boolean | false | When true, the tooltip never opens. Useful for conditional hints. |
| children* | ReactNode | — | Trigger element. Wrapped in a transparent inline-flex span for event listening. |