registry:ui · NativeHTML
tooltip — hover intent on the popover laws
A popover=manual panel (no light dismiss — the tip owns its exit) anchored through CSS Anchor Positioning like popover.svelte, zero JS geometry. The component exists for the one thing CSS alone cannot do: intent — pointerenter opens after a delay, the close delay lets the pointer cross onto the tip, focus opens immediately, Escape closes.
tooltip
Hover the button — the tip shows immediately (a hover-intent delay is opt-in through openDelay), then focus it with the keyboard — two intent paths, one tip. Escape dismisses; moving the pointer onto the tip keeps it open.
<Tooltip text="Copy install command">
<PressButton>copy</PressButton>
</Tooltip>W3C foundation
What the platform gives, what intent adds
The Popover API gives the top layer and manual semantics — Escape becomes the component's job (manual popovers skip the native Esc path; the family wires its own window keydown); CSS Anchor Positioning gives placement with zero JS geometry. The intent model — hover delay, close grace, focus immediacy — is the component's entire reason to exist.
<script lang="ts">
import Tooltip from '@ui/tooltip.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<Tooltip text="Copy install command">
<PressButton>copy</PressButton>
</Tooltip>
<!-- placement + opt-in hover-intent delay -->
<Tooltip text="Below the trigger" placement="bottom" openDelay={150}>…</Tooltip>
<!-- opt-in pointer pin: aimed at the anchor point the placement names -->
<Tooltip text="Aimed at the anchor's top-center" arrow>…</Tooltip>
<Tooltip text="Aimed at the anchor's bottom-end corner" arrow placement="bottom-end">…</Tooltip>types
Tooltip variants
Six placements across the anchored sides; the arrow opts into a notch cut from the bubble itself.
tooltip · variants
<script lang="ts">
import Tooltip from '@ui/tooltip.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<div class="grid w-full gap-4 sm:grid-cols-3">
<div class="border border-border p-4"><Tooltip text="The default anchored side"><PressButton variant="outline">top</PressButton></Tooltip></div>
<div class="border border-border p-4"><Tooltip text="For toolbar bottoms" placement="bottom"><PressButton variant="outline">bottom</PressButton></Tooltip></div>
<div class="border border-border p-4"><Tooltip text="Corner aim through the notch" arrow placement="top-end"><PressButton variant="outline">arrow · top-end</PressButton></Tooltip></div>
</div>usage
Usage
Wrap the trigger; the tip is non-interactive by contract — actionable content belongs in a popover.
<script lang="ts">
import Tooltip from '@ui/tooltip.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<Tooltip text="Copy install command">
<PressButton>copy</PressButton>
</Tooltip>
<!-- placement + opt-in hover-intent delay -->
<Tooltip text="Below the trigger" placement="bottom" openDelay={150}>…</Tooltip>
<!-- opt-in pointer pin: aimed at the anchor point the placement names -->
<Tooltip text="Aimed at the anchor's top-center" arrow>…</Tooltip>
<Tooltip text="Aimed at the anchor's bottom-end corner" arrow placement="bottom-end">…</Tooltip>a11y
Accessibility
Hover and focus are two intent paths into one tip; the pairing only reads while the panel is shown.
Keyboard
| Key | Action |
|---|---|
| Tab | Focus opens the tip immediately; focus leaving the trigger closes it |
| Escape | Closes the tip now (manual popovers skip the native Esc path) |
ARIA
| Attribute | Value | Description |
|---|---|---|
| aria-describedby | {id} → panel | The wrapper points at the tip permanently; hidden popover content is display:none, so it only reads while shown. |
| role | tooltip | The popover panel carries the tooltip role. |
| popover | manual | Top-layer panel with no light dismiss — the tip owns its exit. |
theming
Density and tokens
The panel rides the jx-surface paint law and the WAAPI motion kernel; anchor geometry and the notch are token-authored.
| Token | Default | Source |
|---|---|---|
| --jx-tip-{id} | anchor-name | component |
| --jx-tip-gap | 6px | component |
| --jx-tip-notch | 8px | component |
| --jx-tip-shape | authored at open | component |
| --jx-surface-ring / -inner | mask layers | component |
| --jx-surface-in-x/y · -ox/-oy | measured vectors | component |
| --jx-p | parked at 1 | component |
| --jx-hit | 28 / 32 / 40 / 48px | density |
axes
Universal props
The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The hover hint carries its OWN elevation — level1 (1dp, the minimal rung): the theme's level table pairs the shadow recipe with the ladder rung; the carriers stamp the promoted panel root. The axis surface rides the same motion kernel and the notch mask (an arrow tip composes, never replaces).
Tooltip · universal props
<Tooltip text="level1 · the default">…</Tooltip>
<Tooltip text="level3" elevation="level3">…</Tooltip>
<Tooltip text="4dp" elevation={4}>…</Tooltip>api
API
Props extend a plain wrapper span; placements cover the six anchored positions and their corner aims.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| text * | string | — | The tip itself — plain text; composed content belongs in a popover. |
| placement | 'top' | 'bottom' | 'top-start' | 'bottom-start' | 'top-end' | 'bottom-end' | 'top' | Anchored side; the arrow aims at the anchor point the placement names. |
| arrow | boolean | false | Opt-in pointer notch cut from the bubble through its mask. |
| openDelay | number | 0 | Delay before hover opens (ms); 0 shows immediately — focus opens immediately regardless. |
| closeDelay | number | 100 | Grace before closing, so the pointer can cross onto the tip. |
| variant | 'solid' | 'acrylic' | 'auto' | 'auto' · Own default, not ambient | Floating-surface variant; acrylic unless the environment asks for reduced transparency. Defaults: literal slot — own 'auto', ambient when an axis opens. |
| id | string | auto-generated | Panel id; aria-describedby pairs the trigger wrapper to the tip. |
| class | string | '' | Extra classes on the anchor wrapper. |
| children * | Snippet | — | The trigger content; the wrapper span carries the anchoring. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |