Tooltip
A small label that describes its trigger, shown on hover or keyboard focus. Built on reka-ui's Tooltip primitives, so focus, dismissal and aria wiring come for free.
Playground
<Tooltip
text="Save changes"
>
<Button label="Hover me" />
</Tooltip>Default
Pass the label as text and wrap the trigger in the default slot.
<script setup lang="ts">
import { Button, Tooltip } from 'frappe-ui'
</script>
<template>
<Tooltip text="This action cannot be undone" :hover-delay="0" side="top">
<Button theme="red">Delete</Button>
</Tooltip>
</template>Side and offset
side (top / right / bottom / left) picks which edge of the trigger the tooltip sits on; offset sets the gap in px. Both match Popover and HoverCard. The tooltip flips automatically to stay inside the viewport.
Slots
#default is the trigger — Tooltip is the one overlay that reads this way, because <Tooltip text="Delete"><Button /></Tooltip> is the shape almost every call site wants.
#content is the tooltip's content, for anything richer than a string. It renders inside the standard bubble, so you inherit the surface rather than rebuilding it. Add bare when the content brings its own surface — an image preview, say. The arrow renders either way.
<script setup lang="ts">
import { Button, KeyboardShortcut, Tooltip } from 'frappe-ui'
</script>
<template>
<div class="flex items-center gap-4">
<Tooltip side="top">
<template #content>
<span class="flex items-center gap-1">
Delete
<KeyboardShortcut bg combo="Mod+Backspace" class="px-1" />
</span>
</template>
<Button theme="red">Delete</Button>
</Tooltip>
<Tooltip side="top" bare>
<template #content>
<img
src="https://frappeui.com/frappe-ui-logo.svg"
alt="frappe-ui"
class="size-20 rounded-4 bg-surface-white p-2 shadow-xl"
/>
</template>
<Button>Preview</Button>
</Tooltip>
</div>
</template>Grouping (shared hover delay)
Wrap a group of buttons in a TooltipProvider so that once one tooltip is open, moving the pointer to a neighbouring trigger within skip-delay opens its tooltip instantly — no delay between adjacent buttons. Tooltip and tooltip-bearing Buttons automatically reuse a surrounding provider instead of creating their own.
<script setup lang="ts">
import { Button, Tooltip, TooltipProvider } from 'frappe-ui'
</script>
<template>
<!--
Hover any button to open its tooltip after `hover-delay`. Sweeping the
pointer to a neighbour within `skip-delay` opens the next tooltip
instantly — no per-button flicker — because the whole row shares one
tooltip context.
-->
<TooltipProvider :hover-delay="0.5" :skip-delay="0.3">
<div class="flex items-center gap-1">
<Button icon="bold" :tooltip="'Bold'" />
<Button icon="italic" :tooltip="'Italic'" />
<Button icon="underline" :tooltip="'Underline'" />
<Tooltip text="Strikethrough">
<Button icon="minus" />
</Tooltip>
</div>
</TooltipProvider>
</template>Styling
There are no class-injection props. Style the tooltip through the stable data-slot hooks:
| Hook | Element |
|---|---|
[data-slot="content"] | the portaled content (reka TooltipContent) |
[data-slot="bubble"] | the default bubble that owns the visuals (absent under bare) |
[data-slot="arrow"] | the arrow pointing back at the trigger |
:where([data-slot='bubble']) {
/* your overrides */
}Notes
- Set
disabledto suppress the tooltip while still rendering the trigger. Useful when the label only applies in some states. - A tooltip labels its trigger — it is not a place for interactive content. For a panel you can click into, use
HoverCard.
Migrating from v0
placement is now side, and arrowClass is gone — style the arrow through [data-slot="arrow"], or use offset if you were using it to nudge the tooltip's position.
#body is now #content, and it renders inside the bubble. Most #body call sites hand-copied the bubble's own classes to get them back, so the move usually means deleting that wrapper:
<!-- before -->
<Tooltip>
<template #body>
<div class="rounded bg-surface-gray-10 px-2 py-1 text-xs text-ink-base shadow-xl">
<span>Hide password</span>
</div>
</template>
<Button icon="eye" />
</Tooltip>
<!-- after -->
<Tooltip>
<template #content>
<span>Hide password</span>
</template>
<Button icon="eye" />
</Tooltip>If the content genuinely brings its own surface, keep it and add bare.
API Reference
Tooltip
Show types
export type TooltipSide = 'top' | 'right' | 'bottom' | 'left'
export interface TooltipProps {
/**
* Text content shown inside the tooltip.
* Ignored if a default slot is provided.
*/
text?: string
/**
* Delay (in seconds) before showing the tooltip on hover.
*/
hoverDelay?: number
/**
* Side of the trigger the tooltip is placed on.
*/
side?: TooltipSide
/**
* Distance in px between the trigger and the tooltip.
*/
offset?: number
/**
* Render `#content` without the bubble shell. The arrow still renders.
* Use it for content that brings its own surface, like an image preview.
*/
bare?: boolean
/**
* Disables the tooltip entirely. The trigger still renders.
*/
disabled?: boolean
}Text content shown inside the tooltip. Ignored if a default slot is provided.
Delay (in seconds) before showing the tooltip on hover.
Side of the trigger the tooltip is placed on.
Distance in px between the trigger and the tooltip.
Render `#content` without the bubble shell. The arrow still renders. Use it for content that brings its own surface, like an image preview.
Disables the tooltip entirely. The trigger still renders.
| Slot | Payload |
|---|---|
default | — The trigger. Tooltip is the one overlay whose `#default` is the trigger rather than the content — the shorthand `<Tooltip text="…"><Button /></Tooltip>` is the overwhelmingly common shape, and `#content` names the other half. |
content | — The tooltip's content. Takes precedence over `text`. |
The trigger. Tooltip is the one overlay whose `#default` is the trigger rather than the content — the shorthand `<Tooltip text="…"><Button /></Tooltip>` is the overwhelmingly common shape, and `#content` names the other half.
The tooltip's content. Takes precedence over `text`.
TooltipProvider
Show types
export type TooltipSide = 'top' | 'right' | 'bottom' | 'left'
export interface TooltipProps {
/**
* Text content shown inside the tooltip.
* Ignored if a default slot is provided.
*/
text?: string
/**
* Delay (in seconds) before showing the tooltip on hover.
*/
hoverDelay?: number
/**
* Side of the trigger the tooltip is placed on.
*/
side?: TooltipSide
/**
* Distance in px between the trigger and the tooltip.
*/
offset?: number
/**
* Render `#content` without the bubble shell. The arrow still renders.
* Use it for content that brings its own surface, like an image preview.
*/
bare?: boolean
/**
* Disables the tooltip entirely. The trigger still renders.
*/
disabled?: boolean
}Delay (in seconds) before the first tooltip in the group opens.
Window (in seconds) during which moving to another trigger in this group opens its tooltip with no delay. Mirrors reka's `skipDelayDuration`.
When `true`, hovering the tooltip body closes it as the pointer leaves the trigger.
| Slot | Payload |
|---|---|
default | {} |