Tooltip
Floating hint and metadata surface for hover, focus, or click-triggered disclosure.
Overview
Tooltip is the shared floating hint primitive used across @fsai/shared-ui.
It is built on top of Floating UI and exported as a small compound API:
TooltipTooltipTriggerTooltipContentuseTooltip
Use it for short contextual help, labels for icon-only controls, compact metadata previews, and conditional disabled-state explanations.
Basic Usage
import { Tooltip, TooltipContent, TooltipTrigger } from '@fsai/shared-ui';
<Tooltip>
<TooltipTrigger asChild>
<button type="button">Hover me</button>
</TooltipTrigger>
<TooltipContent>Helpful context</TooltipContent>
</Tooltip>By default, the tooltip opens on hover and focus and closes on dismiss.
Trigger Composition
Use TooltipTrigger asChild when the trigger should be an existing element such as a button, link, badge, chart element, or custom control.
<Tooltip placement="right">
<TooltipTrigger asChild>
<Badge color="gray" label="Primary" className="cursor-pointer" />
</TooltipTrigger>
<TooltipContent>#3B82F6</TooltipContent>
</Tooltip>If you do not use asChild, TooltipTrigger renders its own button.
Click-Triggered Help Tooltip
For inline help affordances, use triggerOnClick.
<Tooltip triggerOnClick>
<TooltipTrigger
className="inline-flex h-5 w-5 items-center justify-center rounded-full"
aria-label="Show more information"
>
<CircleInfo className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="max-w-xs text-md leading-5">
Extra guidance for this field
</TooltipContent>
</Tooltip>This is the pattern used in GuidedFlow for optional explanatory help.
Controlled Tooltip State
Use controlled open when the tooltip should reflect component state instead of normal hover behavior.
<Tooltip open={isDragging && showValueInTooltip ? true : false}>
<TooltipTrigger asChild>
<div onMouseDown={startDragging} className="slider-handle" />
</TooltipTrigger>
{showValueInTooltip && (
<TooltipContent>{formatValue(renderedValue)}</TooltipContent>
)}
</Tooltip>This is the pattern used in RangeSlider to show the live value while dragging.
Conditional Or Disabled Tooltips
When tooltip content is optional, disable the tooltip entirely instead of rendering an empty content shell.
<Tooltip disabled={!tooltipText}>
<TooltipTrigger asChild>
<button type="button" aria-label="More actions">
<MoreHorizontal />
</button>
</TooltipTrigger>
{tooltipText && <TooltipContent>{tooltipText}</TooltipContent>}
</Tooltip>This is the same pattern used in components like ModifiersBarButton.
Rich Content Tooltip
TooltipContent can hold more than a plain string when the content is still lightweight and glanceable.
<Tooltip placement="right">
<TooltipTrigger asChild>
<g>{/* chart region */}</g>
</TooltipTrigger>
<TooltipContent className="p-2 w-36 flex flex-col items-start">
<div className="p-1 bg-gray-bg-strong rounded-md mb-2">California</div>
<p className="text-md font-semibold text-strong">{formattedValue}</p>
</TooltipContent>
</Tooltip>This is how GeoChart renders compact region metadata.
Props
Tooltip
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Required. Usually TooltipTrigger and TooltipContent. |
placement | Placement | 'top' | Floating placement. |
initialOpen | boolean | false | Initial uncontrolled open state. |
open | boolean | — | Controlled open state. |
onOpenChange | (open: boolean) => void | — | Controlled open-state callback. |
disabled | boolean | false | Prevents the tooltip from opening. |
triggerOnClick | boolean | false | Adds click-trigger behavior for the trigger. |
TooltipTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Reuses the child element as the trigger instead of rendering a wrapper button. |
children | ReactNode | — | Trigger content or the child element when asChild is used. |
...htmlProps | HTMLProps<HTMLElement> | — | Standard trigger element props. |
Behavior notes:
- when
asChildis used, existing child handlers are merged with tooltip handlers - the trigger receives
data-state="open"ordata-state="closed" - without
asChild, the trigger is rendered as abutton
TooltipContent
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Tooltip body content. |
className | string | — | Additional content classes. |
arrowClassName | string | — | Additional classes for the tooltip arrow. |
...htmlProps | HTMLProps<HTMLDivElement> | — | Standard floating content props. |
useTooltip
useTooltip is the low-level hook behind the compound components.
Use it only when you need a custom tooltip composition that cannot be expressed with Tooltip, TooltipTrigger, and TooltipContent directly. Most product code should use the standard components instead.
Common Usage Patterns
| Pattern | Path | Notes |
|---|---|---|
| Disabled-action explanation | fsai/packages/shared-ui/src/primitives/Menu/Menu.primitives.tsx | Menu items use tooltips for disabled-item context. |
| Primitive-powered avatar labels | fsai/packages/shared-ui/src/primitives/Avatar/Avatar.tsx | Avatar composes Tooltip internally for optional name disclosure. |
| Click-triggered field help | fsai/packages/shared-ui/src/components/GuidedFlow/GuidedFlow.tsx | Info icon opens a help tooltip on click. |
| Controlled dragging value | fsai/packages/shared-ui/src/components/RangeSlider/RangeSlider.tsx | Tooltip visibility is driven by drag state. |
| Optional sidebar label | fsai/packages/shared-ui/src/components/DashboardSidebar/DashboardSidebar.tsx | Existing nodes are wrapped with TooltipTrigger asChild. |
| Rich chart hover card | fsai/packages/shared-ui/src/components/Charts/GeoChart.tsx | Tooltip content contains structured metadata, not just a string. |
Important Conventions
- Prefer
TooltipTrigger asChildwhen the trigger is already a real interactive or semantic element. - Keep tooltip content short and glanceable. If the content becomes workflow-level or action-heavy, use another pattern.
- Use
triggerOnClickfor help or info affordances that should be deliberate rather than hover-only. - Disable the tooltip when there is no content instead of rendering an empty
TooltipContent. - The trigger gets
data-state, so open styling can be driven from CSS utilities. TooltipContentis portaled and only renders while open.Tooltipis not a replacement for a persistent label or accessible field description.
Guidelines
- Do use tooltips for concise supplemental context
- Do use
asChildfor existing buttons, links, badges, and custom nodes - Do control
openwhen the tooltip should follow component state - Do keep the content lightweight even when it includes structured markup
- Don't use a tooltip as the only way to understand a core workflow action
- Don't render empty or meaningless tooltip shells
- Don't use tooltip content as a substitute for accessible labels on icon-only controls