UNPKG

@loke/ui

Version:
242 lines (193 loc) 7.29 kB
--- name: tooltip type: core domain: overlays requires: [loke-ui] description: > Hover/focus tooltips. TooltipProvider, Tooltip, TooltipTrigger, TooltipPortal, TooltipContent, TooltipArrow. Requires TooltipProvider ancestor. Default delay 700ms, skip-delay 300ms. data-state: closed | delayed-open | instant-open. CSS vars: --loke-tooltip-content-available-height, -width, -transform-origin, --loke-tooltip-trigger-height, -width. --- # Tooltip ## Setup Minimum working tooltip. `TooltipProvider` must wrap all `Tooltip` usage — place it near the root of your app or layout. ```tsx import { Tooltip, TooltipArrow, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, } from "@loke/ui/tooltip"; function Example() { return ( <TooltipProvider> <Tooltip> <TooltipTrigger>Hover me</TooltipTrigger> <TooltipPortal> <TooltipContent> Save document <TooltipArrow /> </TooltipContent> </TooltipPortal> </Tooltip> </TooltipProvider> ); } ``` `TooltipContent` renders inside a hidden `role="tooltip"` element for screen readers via `aria-describedby` on the trigger. The visible content and the hidden tooltip element both receive the same text (or `aria-label` if provided). Default open delay: 700ms. Skip-delay window: 300ms (moving between triggers skips the delay). ## Core Patterns ### Custom delay Override delay at the provider level or per-tooltip. ```tsx import { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, } from "@loke/ui/tooltip"; // Provider-level: all tooltips use 400ms function App() { return ( <TooltipProvider delayDuration={400} skipDelayDuration={150}> <MyLayout /> </TooltipProvider> ); } // Per-tooltip override: this one is instant function InstantTooltip() { return ( <TooltipProvider> <Tooltip delayDuration={0}> <TooltipTrigger>Icon button</TooltipTrigger> <TooltipPortal> <TooltipContent>Delete row</TooltipContent> </TooltipPortal> </Tooltip> </TooltipProvider> ); } ``` `delayDuration={0}` opens the tooltip immediately on hover, which sets `data-state="instant-open"` rather than `"delayed-open"`. ### Simple text tooltip with aria-label When the trigger already has visible text that differs from the tooltip, use `aria-label` on `TooltipContent` to provide a distinct accessible description without duplicating visible content. ```tsx import { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, } from "@loke/ui/tooltip"; function KeyboardShortcutHint() { return ( <TooltipProvider> <Tooltip> <TooltipTrigger>Save</TooltipTrigger> <TooltipPortal> <TooltipContent aria-label="Save document (Ctrl+S)"> Ctrl+S </TooltipContent> </TooltipPortal> </Tooltip> </TooltipProvider> ); } ``` The hidden `role="tooltip"` element renders `aria-label` text ("Save document (Ctrl+S)") while the visible content shows "Ctrl+S". ### Hoverable content (links, buttons inside tooltip) By default, `TooltipProvider` uses `disableHoverableContent={false}`, meaning the pointer can move from the trigger to the tooltip content without it closing. A convex hull grace area is computed between trigger and content. To allow interactive content inside a tooltip-like overlay, use `Popover` instead — tooltips close on click and do not support `role="dialog"` interaction patterns. For informational hoverable content only: ```tsx import { Tooltip, TooltipContent, TooltipPortal, TooltipProvider, TooltipTrigger, } from "@loke/ui/tooltip"; function HoverableTooltip() { return ( <TooltipProvider disableHoverableContent={false}> <Tooltip> <TooltipTrigger>Status</TooltipTrigger> <TooltipPortal> <TooltipContent> Last synced 2 minutes ago </TooltipContent> </TooltipPortal> </Tooltip> </TooltipProvider> ); } ``` Set `disableHoverableContent={true}` on `TooltipProvider` or `Tooltip` if you want the tooltip to close as soon as the pointer leaves the trigger. ## Common Mistakes ### Missing TooltipProvider wrapper `Tooltip` reads delay timers and skip-delay state from `TooltipProviderContext`. Without a `TooltipProvider` ancestor, the context hook throws at runtime. ```tsx // Wrong — no provider, throws in runtime function App() { return ( <Tooltip> <TooltipTrigger>Hover me</TooltipTrigger> <TooltipContent>Info</TooltipContent> </Tooltip> ); } // Correct function App() { return ( <TooltipProvider> <Tooltip> <TooltipTrigger>Hover me</TooltipTrigger> <TooltipPortal> <TooltipContent>Info</TooltipContent> </TooltipPortal> </Tooltip> </TooltipProvider> ); } ``` One `TooltipProvider` at the app root is sufficient. You do not need a provider per tooltip. Source: `src/components/tooltip/tooltip.tsx``useTooltipProviderContext` requires the context. ### Using Tooltip for interactive content `TooltipTrigger` closes the tooltip on `onClick` and `onPointerDown`. Interactive content (buttons, links, forms) inside a tooltip-style overlay belongs in a `Popover`, which uses `role="dialog"`, supports focus trapping, and does not close on pointer-down. Source: `src/components/tooltip/tooltip.tsx``onClick` and `onPointerDown` call `context.onClose`. ### Missing TooltipPortal around TooltipContent Without `TooltipPortal`, `TooltipContent` renders inline in the DOM. This causes z-index stacking failures and clipping by `overflow: hidden` ancestors — commonly seen inside table cells, cards, or modals. ```tsx // Wrong — may be clipped or appear behind content <Tooltip> <TooltipTrigger>Help</TooltipTrigger> <TooltipContent>Explanation</TooltipContent> </Tooltip> // Correct <Tooltip> <TooltipTrigger>Help</TooltipTrigger> <TooltipPortal> <TooltipContent>Explanation</TooltipContent> </TooltipPortal> </Tooltip> ``` Source: `src/components/tooltip/tooltip.tsx`. ### Not understanding data-state delayed-open vs instant-open `TooltipTrigger` has three `data-state` values: `"closed"`, `"delayed-open"`, and `"instant-open"`. CSS selectors targeting only `open` miss both open states. CSS selectors targeting `data-state="open"` match nothing — there is no `"open"` value. ```css /* Wrong — no data-state="open" on TooltipTrigger */ [data-state="open"] { opacity: 1; } /* Correct — both open states */ [data-state="delayed-open"], [data-state="instant-open"] { opacity: 1; } ``` `instant-open` fires when the skip-delay window is active (user moved quickly from another tooltip). `delayed-open` fires after the normal delay expires. Source: `src/components/tooltip/tooltip.tsx``stateAttribute` computation in `Tooltip`. ## Cross-references - See also: [popover](../popover/SKILL.md) — for interactive floating content (forms, buttons) - See also: [choosing-the-right-component](../choosing-the-right-component/SKILL.md) — Tooltip vs Popover decision - See also: [overlay-infrastructure](../overlay-infrastructure/SKILL.md) — Popper, Presence, DismissableLayer internals