Keyboard shortcut: Ctrl + K
Get started

Tooltip

A short description of the element under the pointer, or under keyboard focus. It is a description and never a container: nothing inside it can be interacted with.

Usage

vue
<script setup lang="ts">
import { VButton, VTooltip } from 'vectis-ui'
</script>

<template>
  <VTooltip text="Copy to the clipboard">
    <template #default="{ triggerProps }">
      <VButton variant="outline" tone="neutral" v-bind="triggerProps">Copy</VButton>
    </template>
  </VTooltip>
</template>

Examples

Placements

placement offers eight sides, four of them aligned to an edge of the trigger rather than centred on it. It names a preference: the browser moves the panel when the side asked for has no room.

vue
<script setup lang="ts">
import { VButton, VTooltip, type TooltipPlacement } from 'vectis-ui'

const placements: TooltipPlacement[] = [
  'top',
  'top-start',
  'top-end',
  'bottom',
  'bottom-start',
  'bottom-end',
  'left',
  'right',
]
</script>

<template>
  <!-- Eight sides, four of them aligned to an edge of the trigger rather than centred
       on it. The placement names a preference and not a position: with no room on that
       side the browser moves the panel by itself. -->
  <div class="row">
    <VTooltip
      v-for="placement in placements"
      :key="placement"
      :text="`Placed ${placement}`"
      :placement="placement"
    >
      <template #default="{ triggerProps }">
        <VButton variant="outline" tone="neutral" v-bind="triggerProps">{{ placement }}</VButton>
      </template>
    </VTooltip>
  </div>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-6) var(--vectis-space-4);
  /* Room above and below for the panels, which are drawn outside this box. */
  padding-block: var(--vectis-space-8);
}
</style>

At the edge of the screen

With no room on the side it asked for, the panel takes the opposite one. It is pure CSS, with no measurement and no observer.

vue
<script setup lang="ts">
import { VButton, VTooltip } from 'vectis-ui'
</script>

<template>
  <!-- The flip is pure CSS and costs no JavaScript: the panel names the side it would
       rather be on, and the browser falls back to the opposite one when the viewport
       has no room there. Scroll these buttons up against the top and the bottom of the
       window to see it happen. -->
  <div class="demo">
    <VTooltip text="Asks for the top, takes the bottom near the edge" placement="top">
      <template #default="{ triggerProps }">
        <VButton variant="outline" tone="neutral" v-bind="triggerProps"> Prefers the top </VButton>
      </template>
    </VTooltip>

    <VTooltip text="Asks for the bottom, takes the top near the edge" placement="bottom">
      <template #default="{ triggerProps }">
        <VButton variant="outline" tone="neutral" v-bind="triggerProps">
          Prefers the bottom
        </VButton>
      </template>
    </VTooltip>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-4);
}
</style>

Opening and closing

delay is how long the pointer has to rest on the trigger before the tooltip appears, 300ms by default, and 0 removes the wait. Keyboard focus opens it at once, Escape closes it without taking the focus away, and pressing the trigger closes it too.

vue
<script setup lang="ts">
import { VButton, VTooltip } from 'vectis-ui'
</script>

<template>
  <div class="demo">
    <!-- The pointer has to rest on the trigger before the tooltip appears, 300ms by
         default, so passing over something on the way somewhere else opens nothing.
         Keyboard focus opens it at once instead: the intent is not in doubt there. -->
    <VTooltip text="Appears after the default wait">
      <template #default="{ triggerProps }">
        <VButton variant="outline" tone="neutral" v-bind="triggerProps">300ms</VButton>
      </template>
    </VTooltip>

    <VTooltip text="Appears after a longer wait" :delay="800">
      <template #default="{ triggerProps }">
        <VButton variant="outline" tone="neutral" v-bind="triggerProps">800ms</VButton>
      </template>
    </VTooltip>

    <!-- A delay of 0 disables the wait entirely. -->
    <VTooltip text="Appears at once" :delay="0">
      <template #default="{ triggerProps }">
        <VButton variant="outline" tone="neutral" v-bind="triggerProps">No wait</VButton>
      </template>
    </VTooltip>

    <!-- Tab to any of these and the tooltip opens immediately; Escape closes it
         without taking the focus away, and pressing the trigger closes it too. -->
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-4);
}
</style>

Describing, not naming

The tooltip sets aria-describedby on its trigger, which keeps its own accessible name: an icon button still carries its label. A tap opens nothing, so whatever the tooltip says has to exist somewhere else as well.

vue
<script setup lang="ts">
import { VIconButton, VTooltip } from 'vectis-ui'
import { close, description, search } from 'vectis-ui/icons'
</script>

<template>
  <div class="demo">
    <!-- The tooltip DESCRIBES and never names: it sets `aria-describedby` on the
         trigger, and the trigger keeps its own accessible name. An icon button
         therefore still carries its `label`, and the tooltip is the visual
         confirmation of it rather than a replacement. -->
    <VTooltip text="Search everything">
      <template #default="{ triggerProps }">
        <VIconButton
          v-bind="triggerProps"
          :icon="search"
          label="Search"
          variant="outline"
          tone="neutral"
        />
      </template>
    </VTooltip>

    <VTooltip text="Export the current view as a CSV file">
      <template #default="{ triggerProps }">
        <VIconButton
          v-bind="triggerProps"
          :icon="description"
          label="Export"
          variant="outline"
          tone="neutral"
        />
      </template>
    </VTooltip>

    <!-- A tap opens nothing, there being no hover on a touch screen and no room for a
         panel standing over what the tap just opened. So whatever the tooltip says has
         to exist somewhere else too: a visible label, a field hint, a panel. -->
    <VTooltip text="Discard the draft">
      <template #default="{ triggerProps }">
        <VIconButton
          v-bind="triggerProps"
          :icon="close"
          label="Discard"
          variant="outline"
          tone="danger"
        />
      </template>
    </VTooltip>
  </div>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-4);
}
</style>

Rich content

The #content slot wins over the text prop when both are given. It has to stay non-interactive: nothing inside can be reached from the keyboard, and the description is flattened to plain text for a screen reader.

vue
<script setup lang="ts">
import { VButton, VHotkeys, VTooltip, VTypography } from 'vectis-ui'
</script>

<template>
  <!-- The slot wins over the `text` prop when both are given. It has to stay
       non-interactive: the tooltip closes as soon as the pointer leaves the trigger,
       so a link inside could never be reached, and the description is flattened to
       plain text for a screen reader anyway. Anything one can act on belongs in a
       panel that stays open. -->
  <VTooltip placement="bottom-start">
    <template #default="{ triggerProps }">
      <VButton variant="outline" tone="neutral" v-bind="triggerProps">Search</VButton>
    </template>

    <template #content>
      <div class="content">
        <VTypography variant="label" as="strong">Global search</VTypography>
        <span>Looks through projects, files and people.</span>
        <!-- The panel is an inverse surface, dark in both themes, and VHotkeys paints
             itself from the colour it inherits: the caps follow it with nothing to
             set. -->
        <span class="shortcut">
          Shortcut
          <VHotkeys keys="mod+k" variant="outline" />
        </span>
      </div>
    </template>
  </VTooltip>
</template>

<style scoped>
.content {
  display: grid;
  gap: var(--vectis-space-1);
  padding-block: var(--vectis-space-1);
}
.shortcut {
  display: flex;
  align-items: center;
  gap: var(--vectis-space-2);
}
</style>

API

Props

PropTypeDefault
textstringnone
What the tooltip says. The #content slot replaces it when both are given.
placementTooltipPlacement'top' | 'top-start' | 'top-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end' | 'right' | 'right-start' | 'right-end''top'
Which side of the element the tooltip appears on. The browser flips it to the opposite side by itself when there is not enough room.
delaynumber300
How long the pointer must rest on the element before the tooltip appears, in milliseconds. Keyboard focus opens it at once, the intent not being in doubt there, and a delay of 0 disables the wait entirely.

Slots

SlotType
default{ triggerProps: TooltipTriggerProps; }
The element the tooltip describes. Bind the triggerProps it receives onto it, which is what ties the two together for assistive technology, and make sure it is something that can take focus, or keyboard users will never see the tooltip.
content{}
Content richer than a plain string: formatting, a keyboard shortcut, an icon. It must stay non-interactive: the description is flattened to plain text for screen readers, and nothing inside can be reached from the keyboard. Content one can interact with belongs in a panel that stays open, such as VMenu.

Types

The types the tables above name, written as the library declares them. The ones carrying export can be imported from vectis-ui to type your own code; the others are the shape of what a slot hands out.

export type TooltipTriggerProps = {
  'aria-describedby': string
}

CSS variables

TokenValue
--vectis-control-size-tooltip-max18rem