Skip to content

Tooltip

In review
Live · web

Hover or Tab to a target: a brief hint floats in after a short delay

The tip shows on hover or keyboard focus (Tab through the row), points at the control with a beak, and dismisses on leave, blur, or Esc. It is non-interactive: you cannot click into it.

NockerlIcon-only buttons
Longer tip
Inline definition
Immediate
Disabled (wrapped)

Tip shown 0 times · placement auto · ~600ms delay · hover + keyboard both work; the island is live.

// Web: React, consuming @dizyx/nockerl-tokens. (Web is the laggard platform;
// this is the canonical API the published @dizyx/nockerl-react package exposes.)
// NockerlTooltip is HEADLESS: it owns the open delay + the anchor → flip → clamp → beak
// engine, and hands a render-prop back {{ stageRef, triggerProps, tip }}. You mount
// your OWN triggers inside a contained stage (stageRef) and wire each via
// triggerProps(cfg); the tip floats as the LAST child INSIDE the stage. Each hint is
// non-interactive (text only); for a panel you tab into, use NockerlPopover instead.
import { NockerlTooltip, NockerlIconButton, NockerlIcon } from '@dizyx/nockerl-react';
// Glyphs are inline SVG nodes (the NockerlIcon registry only carries a few named paths, so
// pass a bespoke `path`/children for the rest).
const CopyIcon = <NockerlIcon><path d="M9 9h11v11H9z" /><path d="M5 15V5a2 2 0 0 1 2-2h10" /></NockerlIcon>;
const StarIcon = <NockerlIcon path="M12 3l2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.9 6.8 19.2l1-5.8L3.5 9.2l5.9-.9z" />;
const InfoIcon = <NockerlIcon><path d="M12 16v-4" /><path d="M12 8h.01" /><path d="M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Z" /></NockerlIcon>;
const TrashIcon = <NockerlIcon><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><path d="M6 6v14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V6" /></NockerlIcon>;
export function SessionToolbar() {
return (
// `place` forces a side (or 'auto' = each trigger's own); `delayMs` is the open delay.
<NockerlTooltip place="auto" delayMs={600}>
{({ stageRef, triggerProps, tip }) => (
// The contained STAGE: every tip floats INSIDE here, clamped to it (never the page).
<div className="toolbar" ref={stageRef} style={{ position: 'relative' }}>
{/* NockerlIcon-only buttons are the canonical use. The tip NAMES the action. Each
trigger declares its own hint via triggerProps({{ key, text, side, ... }}). */}
<NockerlIconButton
icon={CopyIcon}
label="Copy link"
onClick={copyLink}
{...triggerProps({ key: 'copy', text: 'Copy link', side: 'bottom', multiline: false, width: 220 })}
/>
<NockerlIconButton
icon={StarIcon}
label="Add to favorites"
onClick={favorite}
{...triggerProps({ key: 'star', text: 'Add to favorites', side: 'bottom', multiline: false, width: 220 })}
/>
{/* A longer hint wraps (multiline); `side` is a preference, so it flips/clamps to fit. */}
<NockerlIconButton
icon={InfoIcon}
label="About context usage"
{...triggerProps({
key: 'info',
text: 'Context window usage across this session, including system prompt and tool results.',
side: 'left',
multiline: true,
width: 240,
})}
/>
{/* A disabled control fires no hover/focus; wire an enabled wrapper so the tip still shows. */}
<span
tabIndex={0}
role="button"
aria-disabled="true"
aria-label="Delete"
{...triggerProps({ key: 'del', text: 'Finish the running task before deleting', side: 'top', multiline: true, width: 220 })}
>
<NockerlIconButton icon={TrashIcon} label="Delete" disabled />
</span>
{/* The live tip: render it as the LAST child INSIDE the stage. */}
{tip}
</div>
)}
</NockerlTooltip>
);
}

Tooltip is headless: it owns the open delay + the anchor → flip → clamp → beak engine and returns a render-prop. The per-hint text / side / wrapping are declared on each trigger through triggerProps(cfg), not on Tooltip.

PropTypeDefaultDescription
place'auto' | Side'auto'Forced placement, or auto to use each trigger's own preferred side.
delayMsnumber600Open delay in ms before a hovered/focused tip floats in (0 = instant).
children *(args: NockerlTooltipRenderArgs) => ReactNodeRender the stage + triggers; receives { stageRef, triggerProps, shows }.

Each trigger declares its own hint via triggerProps(cfg: TooltipConfig):

triggerProps(cfg)TypeDefaultDescription
key *stringIdentity of the trigger that owns the open tip (so exactly one tip shows at a time).
text *stringThe hint. Plain text only, because a tooltip is non-interactive.
side *'top' | 'bottom' | 'left' | 'right'Preferred side for this trigger (used when Tooltip place="auto"). Flips / clamps to fit.
multiline *booleanAllow wrapping for a longer tip (otherwise a single line).
width *numberMax width hint (px) for the bubble, so it stays compact.
Tooltip vs. Popover (and cross-platform drift)

A tooltip is a brief, non-interactive hint shown on hover/focus after a delay; you cannot click into it. A popover is a rich, anchored panel you tab into (it holds text, fields, actions). When a hint needs a control or more than a sentence, reach for the popover. Voice’s InfoTip is exactly that: an instant, themed popover used because the native .help() tooltip has a fixed ~2s delay and no styling.

  • Trigger & delay: web shows on hover and keyboard focus (configurable delay); macOS .help() is hover-only with a fixed ~2s system delay; Compose PlainTooltip adds long-press for touch.
  • Surface: web + Compose render an inverted/high-contrast bubble (the one place a surface flips); macOS uses the system tooltip surface (not themeable).
  • Adoption (ratified): TooltipBox is the confirmed path for icon-only Android actions (persistent-label a11y). Until a surface adopts it, contentDescription is what names an icon-only action.