Component
Tooltip
Reveal brief contextual information about a UI element on hover, focus, or tap. Nexus provides Tooltip for Desktop / Tablet and MobileTooltip for Mobile Application, both wired to the --components-tooltips-* tokens.
Playground
Size
Position
Label text
Preview
This is a Tooltip
JSX
<Tooltip
size="sm"
position="bottom-left"
label="This is a Tooltip"
>
<button aria-label="More info">i</button>
</Tooltip>Props
The full Tooltip API for Desktop / Tablet. Use size="sm" for a single label, or size="lg" for a title with optional support content.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "lg" | "sm" | Tooltip variant — controls layout and sizing. SM shows a single label and hugs its content (min 48px / max 320px); LG shows a title with optional support content and a close icon at a fixed 280px width (min 88px / max 560px). |
position | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" | "left-center" | "right-center" | "bottom-left" | Which edge the arrow sits on (and its alignment) relative to the trigger. 8 positions; the 20×12 arrow is inset 12px from the corner and overlaps the bubble by 2px internally. |
label | string | "This is a Tooltip" | SM only — tooltip text. Truncates at 2 lines. |
title | string | "This is a Tooltip Title" | LG only — tooltip heading text. Truncates at 2 lines. |
supportContent | string | "Tooltips are used to describe or identify an element…" | LG only — supporting description text. Truncates at 3 lines. |
showSupportContent | boolean | true | LG only — show / hide the support description. |
childrenRequired | ReactNode | — | The focusable trigger element the tooltip wraps. The tooltip opens on hover and focus. |
Usage guidelines — Desktop / Tablet
When to use Tooltip
- Reveal brief context on hover or focus — icon buttons with no label, truncated text, or a data value that needs a short explanation.
- Use size='sm' with a single label; use size='lg' for a title with optional support content.
- Always wrap a focusable trigger (button, link, or tabIndex={0}) so keyboard users can open it.
- Keep copy concise — SM truncates at 2 lines, LG support content at 3.
When not to use Tooltip
- Do not put critical information in a tooltip — make it permanent UI (e.g. FormField hint text) instead.
- Do not place interactive content (forms, buttons) inside — use a Popover or Modal.
- Do not pass title or supportContent to an SM tooltip, or label to an LG tooltip.
- Do not override the colors or the -2px arrow offset, and never use Tooltip on mobile touch screens.
Usage guidelines — Mobile Application
When to use MobileTooltip
- Use on Mobile Application screens to nudge people toward a UI element via support content.
- Choose one of the 6 top / bottom positions and ensure the trigger is at least 44×44px.
- Keep hint text to 3 lines or fewer — it truncates beyond that.
- Rely on tap / long-press to open and a second tap (or tap outside) to dismiss.
When not to use MobileTooltip
- Do not use the desktop Tooltip on mobile — hover triggers are unreliable on touch.
- Do not pass title, label, or size — MobileTooltip has support content and a single fixed layout only.
- Do not use left-center or right-center — those positions are desktop-only.
- Do not rely on Escape or keyboard focus to open or dismiss it on mobile.
src/components/tooltip-bubble.tsx
/**
* Presentational Tooltip renderers for the showcase page.
*
* The Nexus `Tooltip` / `MobileTooltip` components are spec-only (not yet in
* `nexus/components`), so this file renders the Figma bubble with token-styled
* HTML. // TODO: swap these out for the real Nexus components once they ship.
*
* Faithful to the Figma nodes 438-3534 (Desktop/Tablet) and 438-3710 (Mobile):
* - dark background (`--components-tooltips-color-background-dark`), white text
* - `shadow-lg` token (matches Figma "Dropshadow/shadow/shadow-lg")
* - the arrow is the Figma 20×12 vector (12×20 on the side positions), inset
* 12px from the bubble edge on desktop / 16px on mobile (the Arrow Frame's
* horizontal padding) and tucked 2px under the body (Figma's -2px gap).
* - widths: SM hugs (min 48 / max 320), LG is fixed 280 wide (min 88 / max 560
* per the node), Mobile hugs (min 124 / max 420).
*
* Radius note: the Figma node renders BOTH desktop sizes with `round-4` (4px),
* so we follow the node over the spec prose (which quotes rounded-6 for LG).
*/
import { CloseOutlineIcon } from "nexus/icons";
export type TooltipSize = "sm" | "lg";
export type TooltipPosition =
| "top-left"
| "top-center"
| "top-right"
| "bottom-left"
| "bottom-center"
| "bottom-right"
| "left-center"
| "right-center";
export type MobileTooltipPosition = Exclude<
TooltipPosition,
"left-center" | "right-center"
>;
const BG = "bg-[var(--components-tooltips-color-background-dark)]";
const TEXT = "text-[var(--components-tooltips-color-text-white)]";
type ArrowDirection = "up" | "down" | "left" | "right";
const DIRECTION_BY_POSITION: Record<TooltipPosition, ArrowDirection> = {
"top-left": "up",
"top-center": "up",
"top-right": "up",
"bottom-left": "down",
"bottom-center": "down",
"bottom-right": "down",
"left-center": "left",
"right-center": "right",
};
/** The Figma arrow vector: 20×12 on top/bottom edges, 12×20 on the sides. */
const ARROW_GEOMETRY: Record<
ArrowDirection,
{ width: number; height: number; d: string }
> = {
down: { width: 20, height: 12, d: "M0 0h20l-8.9 10.86a1.5 1.5 0 0 1-2.2 0L0 0Z" },
up: { width: 20, height: 12, d: "M0 12h20L11.1 1.14a1.5 1.5 0 0 0-2.2 0L0 12Z" },
left: { width: 12, height: 20, d: "M12 0v20L1.14 11.1a1.5 1.5 0 0 1 0-2.2L12 0Z" },
right: { width: 12, height: 20, d: "M0 0v20l10.86-8.9a1.5 1.5 0 0 0 0-2.2L0 0Z" },
};
/**
* Placement classes per position: the arrow sits just outside the bubble edge
* (`top-full` / `bottom-full` / `left-full` / `right-full`) and a -2px margin
* pulls it under the body — Figma's "-2px vertical gap between objects". The
* inset from the corner is the Arrow Frame's horizontal padding: 12px on
* desktop, 16px on mobile. Full static literals so Tailwind emits them.
*/
const DESKTOP_ARROW: Record<TooltipPosition, string> = {
"top-left": "bottom-full -mb-2 left-12",
"top-center": "bottom-full -mb-2 left-1/2 -translate-x-1/2",
"top-right": "bottom-full -mb-2 right-12",
"bottom-left": "top-full -mt-2 left-12",
"bottom-center": "top-full -mt-2 left-1/2 -translate-x-1/2",
"bottom-right": "top-full -mt-2 right-12",
"left-center": "right-full -mr-2 top-1/2 -translate-y-1/2",
"right-center": "left-full -ml-2 top-1/2 -translate-y-1/2",
};
const MOBILE_ARROW: Record<MobileTooltipPosition, string> = {
"top-left": "bottom-full -mb-2 left-16",
"top-center": "bottom-full -mb-2 left-1/2 -translate-x-1/2",
"top-right": "bottom-full -mb-2 right-16",
"bottom-left": "top-full -mt-2 left-16",
"bottom-center": "top-full -mt-2 left-1/2 -translate-x-1/2",
"bottom-right": "top-full -mt-2 right-16",
};
const Arrow = ({
position,
placement,
}: {
position: TooltipPosition;
placement: string;
}) => {
const { width, height, d } = ARROW_GEOMETRY[DIRECTION_BY_POSITION[position]];
return (
<svg
aria-hidden
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
className={`absolute ${placement} fill-[var(--components-tooltips-color-background-dark)]`}
>
<path d={d} />
</svg>
);
};
/** Desktop / Tablet tooltip bubble (SM = label · LG = title + support). */
export const TooltipBubble = ({
size,
position,
label,
title,
supportContent,
showSupportContent,
}: {
size: TooltipSize;
position: TooltipPosition;
label: string;
title: string;
supportContent: string;
showSupportContent: boolean;
}) => (
<div role="tooltip" className="relative inline-flex">
<Arrow position={position} placement={DESKTOP_ARROW[position]} />
{size === "sm" ? (
<div
className={`relative flex items-center justify-center rounded-4 px-12 py-8 shadow-lg min-w-[48px] max-w-[320px] ${BG}`}
>
<span className={`typography-l1-medium-12 line-clamp-2 break-words ${TEXT}`}>
{label}
</span>
</div>
) : (
<div
className={`relative flex flex-col gap-4 rounded-4 p-12 shadow-lg w-[280px] min-w-[88px] max-w-[560px] ${BG}`}
>
{/* `pr-20` clears the absolute close icon so the title never runs under it. */}
<span
className={`typography-l1-semibold-12 line-clamp-2 break-words pr-20 ${TEXT}`}
>
{title}
</span>
{showSupportContent && supportContent && (
<span
className={`typography-l1-regular-12 line-clamp-3 break-words ${TEXT}`}
>
{supportContent}
</span>
)}
<CloseOutlineIcon
aria-label="Close tooltip"
width={16}
height={16}
className="absolute right-8 top-8 size-16 [&_path]:fill-[var(--components-tooltips-color-icon-white)]"
/>
</div>
)}
</div>
);
/** Mobile Application tooltip bubble (support content only, 6 positions). */
export const MobileTooltipBubble = ({
position,
supportContent,
}: {
position: MobileTooltipPosition;
supportContent: string;
}) => (
<div role="tooltip" className="relative inline-flex">
<Arrow position={position} placement={MOBILE_ARROW[position]} />
<div
className={`relative flex rounded-6 p-16 shadow-lg min-w-[124px] max-w-[420px] ${BG}`}
>
<span
className={`typography-mobile-b1-regular-14 line-clamp-3 break-words ${TEXT}`}
>
{supportContent}
</span>
</div>
</div>
);