GrTooltip

Package: @feugene/granularitycoreGroup: navigation

Shows a short hint on hover or focus.

Machine-translated from the Russian original, not yet reviewed. Read the original

When to take it

  • a label for an icon button — the only way to give a name to a control that has no visible text;
  • the expansion of an abbreviation or a status — a short text that would otherwise take up room in the layout;
  • the hint appears on focus as well — not only on hover, so it is reachable from the keyboard;
  • there is little text — one or two lines: a long text in a hint can neither be selected nor scrolled.

When to take something else

NeedTake
Interaction is needed inside: a link, a button, a fieldGrPopover
A list of actionsGrDropdownMenu
A message the user has to noticeGrAlert
A message about the result of an actionGrToaster
A permanent explanation under a fieldGrFormField

A hint cannot be the only carrier of information. On a touch device there is no hovering, and a text available only through it does not exist at all for some users. What the task cannot be completed without lives in the layout itself — as a label, as the hint of a field or as a line beside it.

The trigger and the tab order

Without a slot the trigger is an info icon, and the wrapper becomes the Tab stop. With a slot the wrapper steps aside: if there is a focusable element inside (a button, a link, a field), aria-describedby travels onto it, and the wrapper loses its tabindex. Otherwise one visual control would take two tab stops, and the outer one a role-less <span>.

If there is nothing to focus in the slot (text, a picture), the wrapper stays a Tab stop — otherwise the hint would not be available from the keyboard at all.

<!-- The wrapper is transparent: there is one Tab stop — the button itself. -->
<GrTooltip text="Delete irreversibly">
  <GrButton variant="ghost" square><IconTrash /></GrButton>
</GrTooltip>

The content

text is a short string; markup instead of it is the #content slot. Without either the hint does not open: an empty panel is worse than a missing one.

The placement

placement is any side from the @floating-ui scale (top, right-start, …), and offsetPx is the gap to the trigger. The resulting side may differ from the one that was set: flip turns the hint over when there is not enough room, and shift keeps it from coming off the edge of the screen.

The delays

openDelay and closeDelay (in ms, both 0 by default). A delay before showing is needed where the hints stand densely — on a bar of buttons the hint would otherwise blink on every pass of the cursor. Escape and a click outside close it instantly, bypassing closeDelay: a delay there would read as sticking.

Control from the outside

disabled forbids showing entirely. v-model:open puts the visibility under the control of the owner — the component stops opening itself and only reports the intent through update:open.

Touch

On touch devices hover does not exist, and focus on a tap is not guaranteed, so a tap on the trigger toggles the hint, and a tap outside closes it.

Playground 8

Loading…

Code
<GrTooltip />

Install

npm i @feugene/granularity

Import

import { GrTooltip } from '@feugene/granularity/components/GrTooltip'

API

Props

PropTypedefaultDescription
openboolean | undefinedundefinedControlled visibility. Without it the component runs the visibility itself.
disabledboolean | undefinedfalseThe tooltip is not shown by anything — neither by the cursor, nor by the focus, nor by `v-model:open`.
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe size of the panel and of the default trigger icon.
placementPlacement | undefined"top"The preferred side of the panel; `flip` may change it.
offsetPxnumber | undefinedundefinedThe gap between the trigger and the panel, in px.
openDelaynumber | undefined0The delay before showing, in ms. It saves from flickering while running the cursor along a panel of buttons.
closeDelaynumber | undefined0The delay before hiding, in ms.
textstring | undefinedundefinedThe text of the tooltip. Markup instead of text is the `#content` slot.
iconColorstring | undefined"var(--gr-muted-fg)"The colour of the trigger icon (a CSS colour). By default — `var(--gr-muted-fg)`.

Slots

SlotTypeDescription
defaultanyThe trigger. By default — an info icon.
contentanyThe content of the tooltip; it replaces the `text` prop.

Events

EventTypeDescription
update:open[value: boolean]

Examples 5

Inline help near form labels

Hover or focus the info icon to inspect the help copy.

Inline Help
<script setup lang="ts">
import { GrTooltip } from '@feugene/granularity'
</script>

<template>
  <div class="grid gap-4">
    <label class="inline-flex items-center gap-2 text-sm font-600 text-[var(--gr-fg)]">
      Notification email
      <GrTooltip text="We use this address only for billing alerts and incident updates." />
    </label>

    <div class="rounded-2xl border border-dashed border-[var(--gr-brd)] bg-[var(--gr-muted)]/60 p-4 text-sm text-[var(--gr-muted-fg)]">
      Hover or focus the info icon to inspect the help copy.
    </div>
  </div>
</template>

Custom trigger via default slot

Reuse the same tooltip primitive for icon buttons, labels or table headers.

Custom Trigger
<script setup lang="ts">
import { GrTooltip } from '@feugene/granularity'
</script>

<template>
  <div class="flex flex-wrap items-center gap-4">
    <GrTooltip text="Custom slot lets you attach the tooltip to any trigger element.">
      <button
        type="button"
        class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-[var(--gr-brd)] bg-[var(--gr-card)] text-sm font-700 text-[var(--gr-fg)] transition-colors hover:bg-[var(--gr-muted)]"
        aria-label="Open contextual help"
      >
        ?
      </button>
    </GrTooltip>

    <span class="text-sm text-[var(--gr-muted-fg)]">
      Reuse the same tooltip primitive for icon buttons, labels or table headers.
    </span>
  </div>
</template>

Editable copy and icon tone

Custom tone icon-color="var(--gr-warning)"

Tone
<script setup lang="ts">
import { ref } from 'vue'

import { GrInput, GrTooltip } from '@feugene/granularity'

const tooltipText = ref('Escalation policy will be applied to new alerts only.')
const iconColor = ref('var(--gr-warning)')

// Пресеты цвета иконки из палитры GrTone: клик подставляет валидную CSS-переменную
// темы в инпут `icon-color` (раньше в демо был несуществующий `var(--warning)`).
const tonePresets: Array<{ tone: string, value: string }> = [
  { tone: 'primary', value: 'var(--gr-primary)' },
  { tone: 'neutral', value: 'var(--gr-muted-fg)' },
  { tone: 'success', value: 'var(--gr-success)' },
  { tone: 'warning', value: 'var(--gr-warning)' },
  { tone: 'danger', value: 'var(--gr-danger)' },
  { tone: 'info', value: 'var(--gr-info)' },
  { tone: 'slate', value: 'var(--gr-slate)' },
  { tone: 'azure', value: 'var(--gr-azure)' },
]
</script>

<template>
  <div class="grid gap-4">
    <div class="flex flex-wrap items-center gap-3">
      <span class="text-sm font-600 text-[var(--gr-fg)]">
        Custom tone
      </span>
      <GrTooltip :text="tooltipText" :icon-color="iconColor" />
      <code class="rounded bg-[var(--gr-muted)] px-2 py-1 text-xs text-[var(--gr-muted-fg)]">
        icon-color="{{ iconColor }}"
      </code>
    </div>

    <div class="flex flex-wrap items-center gap-2">
      <button
        v-for="preset in tonePresets"
        :key="preset.tone"
        type="button"
        class="inline-flex items-center gap-2 rounded-full border border-[var(--gr-brd)] px-3 py-1 text-xs font-600 transition-colors hover:bg-[var(--gr-muted)]"
        :class="iconColor === preset.value ? 'ring-2 ring-[var(--gr-ring)]' : ''"
        @click="iconColor = preset.value"
      >
        <span class="h-3 w-3 rounded-full" :style="{ backgroundColor: preset.value }" />
        {{ preset.tone }}
      </button>
    </div>

    <div class="grid gap-3 md:grid-cols-2">
      <GrInput v-model="tooltipText" placeholder="Tooltip text" />
      <GrInput v-model="iconColor" placeholder="var(--gr-warning) / #f59e0b" />
    </div>
  </div>
</template>

Sizes

size="xs"
size="sm"
size="md"
size="lg"

Sizes
<script setup lang="ts">
import { GrTooltip } from '@feugene/granularity'

const sizes = ['xs', 'sm', 'md', 'lg'] as const
</script>

<template>
  <div class="flex flex-wrap items-center gap-6">
    <div v-for="size in sizes" :key="size" class="flex items-center gap-2">
      <span class="text-xs font-semibold text-[var(--gr-muted-fg)]">
        size="{{ size }}"
      </span>

      <GrTooltip :size="size" text="Billing runs on the first day of each month." />
    </div>
  </div>
</template>

Placement

Обёртка не добавляет второй остановки Tab: описание уезжает на саму кнопку, и до подсказки доходит и клавиатура, и скринридер.

Placement
<script setup lang="ts">
import { ref } from 'vue'

import type { GrTooltipPlacement } from '@feugene/granularity'
import { GrButton, GrSegmented, GrSwitch, GrTooltip } from '@feugene/granularity'

const placement = ref<GrTooltipPlacement>('top')
const openDelay = ref(400)
const disabled = ref(false)

const placements = [
  { value: 'top', label: 'top' },
  { value: 'right', label: 'right' },
  { value: 'bottom', label: 'bottom' },
  { value: 'left', label: 'left' },
]

const actions = ['Merge', 'Revert', 'Rebase'] as const
</script>

<template>
  <div class="grid gap-5">
    <div class="flex flex-wrap items-center gap-4">
      <GrSegmented v-model="placement" size="sm" :options="placements" />

      <label class="flex items-center gap-2 text-sm text-[var(--gr-muted-fg)]">
        <GrSwitch v-model="disabled" size="sm" />
        disabled
      </label>
    </div>

    <div class="rounded-2xl border border-[var(--gr-brd)] bg-[var(--gr-card)] p-6">
      <div class="flex flex-wrap items-center gap-2">
        <GrTooltip
          v-for="action in actions"
          :key="action"
          :placement="placement"
          :open-delay="openDelay"
          :close-delay="120"
          :disabled="disabled"
          :text="`${action} — задержка ${openDelay} мс, подсказка не мигает при проведении курсором`"
        >
          <GrButton size="sm" variant="outline">
            {{ action }}
          </GrButton>
        </GrTooltip>
      </div>

      <p class="mt-4 text-sm text-[var(--gr-muted-fg)]">
        Обёртка не добавляет второй остановки Tab: описание уезжает на саму
        кнопку, и до подсказки доходит и клавиатура, и скринридер.
      </p>
    </div>
  </div>
</template>

Component documentationAll components