Frappe UIFrappe UI

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

text
side
hoverDelay
disabled
<Tooltip
  text="Save changes"
>
  <Button label="Hover me" />
</Tooltip>

Default

Pass the label as text and wrap the trigger in the default slot.

vue
<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.

vue
<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.

vue
<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:

HookElement
[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
css
:where([data-slot='bubble']) {
  /* your overrides */
}

Notes

  • Set disabled to 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:

vue
<!-- 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
typescript
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
= ""
string

Text content shown inside the tooltip. Ignored if a default slot is provided.

hoverDelay
= 0.5
number

Delay (in seconds) before showing the tooltip on hover.

side
= "top"
TooltipSide

Side of the trigger the tooltip is placed on.

offset
= 4
number

Distance in px between the trigger and the tooltip.

bare
= false
boolean

Render `#content` without the bubble shell. The arrow still renders. Use it for content that brings its own surface, like an image preview.

disabled
= false
boolean

Disables the tooltip entirely. The trigger still renders.

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`.

TooltipProvider

Show types
typescript
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
}
hoverDelay
= 0.5
number

Delay (in seconds) before the first tooltip in the group opens.

skipDelay
= 0.3
number

Window (in seconds) during which moving to another trigger in this group opens its tooltip with no delay. Mirrors reka's `skipDelayDuration`.

disableHoverableContent
= false
boolean

When `true`, hovering the tooltip body closes it as the pointer leaves the trigger.

default
{}