GrTooltip
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
| Need | Take |
|---|---|
| Interaction is needed inside: a link, a button, a field | GrPopover |
| A list of actions | GrDropdownMenu |
| A message the user has to notice | GrAlert |
| A message about the result of an action | GrToaster |
| A permanent explanation under a field | GrFormField |
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…
<GrTooltip />Install
npm i @feugene/granularityImport
import { GrTooltip } from '@feugene/granularity/components/GrTooltip'API
Props
| Prop | Type | default | Description |
|---|---|---|---|
open | boolean | undefined | undefined | Controlled visibility. Without it the component runs the visibility itself. |
disabled | boolean | undefined | false | The tooltip is not shown by anything — neither by the cursor, nor by the focus, nor by `v-model:open`. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size of the panel and of the default trigger icon. |
placement | Placement | undefined | "top" | The preferred side of the panel; `flip` may change it. |
offsetPx | number | undefined | undefined | The gap between the trigger and the panel, in px. |
openDelay | number | undefined | 0 | The delay before showing, in ms. It saves from flickering while running the cursor along a panel of buttons. |
closeDelay | number | undefined | 0 | The delay before hiding, in ms. |
text | string | undefined | undefined | The text of the tooltip. Markup instead of text is the `#content` slot. |
iconColor | string | undefined | "var(--gr-muted-fg)" | The colour of the trigger icon (a CSS colour). By default — `var(--gr-muted-fg)`. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The trigger. By default — an info icon. |
content | any | The content of the tooltip; it replaces the `text` prop. |
Events
| Event | Type | Description |
|---|---|---|
update:open | [value: boolean] | — |
Examples 5
Inline help near form labels
<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
<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
icon-color="var(--gr-warning)" <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
<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: описание уезжает на саму кнопку, и до подсказки доходит и клавиатура, и скринридер.
<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>