Tooltip

A short note shown on hover or focus.

<script setup lang="ts">
  import { Bold, Italic, Link2, Strikethrough } from '@lucide/vue'
  import { ButtonGroup, IconButton } from '@hina-ui/vue'
</script>

<template>
  <ButtonGroup label="Text formatting">
    <IconButton label="Bold" variant="outline">
      <Bold />
    </IconButton>
    <IconButton label="Italic" variant="outline">
      <Italic />
    </IconButton>
    <IconButton label="Strikethrough" variant="outline">
      <Strikethrough />
    </IconButton>
    <IconButton label="Insert link" variant="outline">
      <Link2 />
    </IconButton>
  </ButtonGroup>
</template>
vue

Usage

import { Tooltip, vTooltip } from '@hina-ui/vue'
ts

The default slot is the trigger and the content prop is the text of the tooltip. It appears on pointer hover or keyboard focus, and goes away when the pointer leaves or Escape is pressed.

<script setup lang="ts">
  import { Button, Tooltip } from '@hina-ui/vue'
</script>

<template>
  <Tooltip content="Publishes as soon as it is saved">
    <Button variant="outline" tone="neutral">Publish</Button>
  </Tooltip>
</template>
vue

IconButton carries a tooltip of its own: label serves as both the accessible name and the tooltip text, so there is no need to wrap one.

Tooltip needs a TooltipProvider above it, and AppShell already includes one. Without a provider the component renders only the trigger.

Examples

Directive

Import vTooltip to apply v-tooltip directly to an existing element. A string binds the text; an object accepts content, side, align, sideOffset, disabled and class. Changes update the tooltip. An empty string, null, undefined or false hides it.

The directive shares TooltipProvider delay and skip-delay state, styling, positioning and keyboard focus behaviour with the component. It preserves existing aria-describedby values and removes its popup and listeners when the element unmounts. Text is rendered as plain text; use the component’s content slot for rich content.

Disable configured tooltip

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Inline, Input, Stack, Switch, Text, vTooltip } from '@hina-ui/vue'

  const content = ref('Tooltip text')
  const disabled = ref(false)
</script>

<template>
  <Stack align="start">
    <Inline>
      <Button v-tooltip="'Bound to the existing element'" variant="outline" tone="neutral">
        String
      </Button>
      <Button v-tooltip="{ content, side: 'bottom', disabled }" variant="outline" tone="neutral">
        Options
      </Button>
    </Inline>
    <Input v-model="content" aria-label="Tooltip text" class="w-64" />
    <Inline>
      <Switch v-model="disabled" aria-label="Disable configured tooltip" />
      <Text>Disable configured tooltip</Text>
    </Inline>
  </Stack>
</template>
vue

Placement

side decides which side the tooltip appears on and align how it lines up with the trigger. The default is centred above. When there is not enough room, it flips to the opposite side.

<script setup lang="ts">
  import { Button, Inline, Tooltip } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Tooltip content="Floats overhead" side="top">
      <Button variant="outline" tone="neutral">top</Button>
    </Tooltip>
    <Tooltip content="Floats to the right" side="right">
      <Button variant="outline" tone="neutral">right</Button>
    </Tooltip>
    <Tooltip content="Floats below" side="bottom">
      <Button variant="outline" tone="neutral">bottom</Button>
    </Tooltip>
    <Tooltip content="Lined up with the start edge" side="top" align="start">
      <Button variant="outline" tone="neutral">top start</Button>
    </Tooltip>
  </Inline>
</template>
vue

Offset

sideOffset is the distance in pixels between the tooltip and its trigger.

<script setup lang="ts">
  import { Button, Inline, Tooltip } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Tooltip content="sideOffset is 0" :side-offset="0">
      <Button variant="outline" tone="neutral">Flush</Button>
    </Tooltip>
    <Tooltip content="sideOffset is 16" :side-offset="16">
      <Button variant="outline" tone="neutral">Further away</Button>
    </Tooltip>
  </Inline>
</template>
vue

Tooltip content

The content slot takes more than text, a shortcut for instance. A line past the maximum width wraps on its own.

<script setup lang="ts">
  import { Info, Save } from '@lucide/vue'
  import { IconButton, Inline, Kbd, Tooltip } from '@hina-ui/vue'
</script>

<template>
  <Inline align="center" class="gap-6">
    <Tooltip>
      <IconButton label="Save" variant="outline" :tooltip="false">
        <Save />
      </IconButton>
      <template #content>
        <Inline align="center" class="gap-1.5">
          Save draft
          <Kbd>Ctrl S</Kbd>
        </Inline>
      </template>
    </Tooltip>
    <Tooltip
      content="A note this long runs past the maximum width of the bubble, so it wraps onto several lines instead of stretching out."
    >
      <IconButton label="Notes" variant="outline" :tooltip="false">
        <Info />
      </IconButton>
    </Tooltip>
  </Inline>
</template>
vue

Delay

delayDuration is how long a hover lasts before the tooltip appears. skipDelayDuration is the window after one closes during which moving to the next trigger skips the delay. Both apply to every Tooltip inside the TooltipProvider.

<script setup lang="ts">
  import { Button, Inline, Tooltip, TooltipProvider } from '@hina-ui/vue'
</script>

<template>
  <TooltipProvider :delay-duration="600" :skip-delay-duration="0">
    <Inline align="center" class="gap-6">
      <Tooltip content="Appears after 600 milliseconds">
        <Button variant="outline" tone="neutral">Slower</Button>
      </Tooltip>
      <Tooltip content="Also waits 600 milliseconds">
        <Button variant="outline" tone="neutral">Another one</Button>
      </Tooltip>
    </Inline>
  </TooltipProvider>
</template>
vue

Controlled

With open set, visibility is up to the caller and hover and keyboard focus no longer apply; a controlled tooltip repositions every frame, so it can follow a moving trigger, which is how the Slider value label works.

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Inline, Switch, Tooltip } from '@hina-ui/vue'

  const shown = ref(true)
</script>

<template>
  <Inline gap="lg" align="center">
    <Tooltip content="Controlled by the switch" :open="shown">
      <Button variant="outline" tone="neutral">Tooltip trigger</Button>
    </Tooltip>
    <Switch v-model="shown">Show tooltip</Switch>
  </Inline>
</template>
vue

Disabling

With disabled, no overlay is built and only the trigger is rendered.

Currently disabled

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Inline, Text, Tooltip } from '@hina-ui/vue'

  const off = ref(true)
</script>

<template>
  <Inline align="center">
    <Tooltip content="Only needed while the sidebar is collapsed" :disabled="off">
      <Button variant="outline" tone="neutral">Hover me</Button>
    </Tooltip>
    <Button size="sm" variant="soft" tone="neutral" @click="off = !off">
      {{ off ? 'Enable' : 'Disable' }} the tooltip
    </Button>
    <Text tone="muted" size="sm">Currently {{ off ? 'disabled' : 'enabled' }}</Text>
  </Inline>
</template>
vue

Behaviour

  • The tooltip appears on focus only when the focus came from the keyboard. Focus left behind by a mouse click, or returned by a closing overlay, does not show it.
  • The tooltip does not lock page scrolling; it follows its trigger as the page scrolls.

Accessibility

  • The trigger carries aria-describedby pointing at the tooltip, so a screen reader reads it after the trigger itself.
  • The tooltip takes no focus and is not in the tab order. Interactive content belongs in a Popover instead.

API

Tooltip

Prop
Type
Default
Description
content
string
—
The text of the tooltip
side
'top' | 'right' | 'bottom' | 'left'
'top'
Which side it appears on
align
'start' | 'center' | 'end'
'center'
How it lines up with the trigger
sideOffset
number
8
Distance from the trigger
open
boolean
—
Controlled visibility; hover and keyboard focus decide when unset
disabled
boolean
false
Whether the tooltip is disabled
class
string
—
Classes appended to the tooltip
Slot
Description
default
The trigger
content
Tooltip content, overrides content

vTooltip

Import vTooltip locally to use v-tooltip in a template, or register it globally with app.directive('tooltip', vTooltip). An ancestor TooltipProvider is required. Native elements and components rendering a single element can be hosts.

TooltipDirectiveValue is string | TooltipDirectiveOptions | null | undefined | false. TooltipDirectiveOptions requires content: string and accepts the Tooltip properties above except open.

TooltipProvider

Prop
Type
Default
Description
delayDuration
number
150
Milliseconds of hover before it appears
skipDelayDuration
number
300
Window in milliseconds that skips the next delay
Slot
Description
default
The subtree sharing these durations