Use it for
Reach for BbTooltip to hang a short label on something the user is already
looking at. What an icon-only button does, what a piece of jargon means, the
full value behind a truncated cell.
Use something else when
BbPopover: the hint holds something to click, or should stay open while the reader works through itBbDropdown: it is a menu of actions
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
A tooltip describes, a popover contains. The bubble never takes focus, so a link or a button inside it is unreachable from a keyboard.
Helpful hints
Keep required guidance visible and use a tooltip for the aside. Pass the label
to text, put the trigger in the activator slot, and spread its props.
<template>
<div class="flex max-w-sm items-end gap-1.5">
<!-- The rule the user must follow is in `description`: on screen, in the
accessibility tree, and printable. The tooltip only adds context. -->
<BbTextInput
id="tooltip-api-key-name"
v-model="keyName"
class="min-w-0 flex-1"
description="Letters, numbers and dashes. Cannot be changed later."
label="API key name"
name="apiKeyName"
/>
<BbTooltip text="The name appears in audit logs next to every request this key makes.">
<!-- A button, not a span: a tooltip on a non-focusable element opens on
hover only, so keyboard users never see it. -->
<template #activator="{ props }">
<BbButton
v-bind="props"
aria-label="About API key names"
class="shrink-0"
icon="lucide:circle-question-mark"
variant="ghost"
/>
</template>
</BbTooltip>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton, BbTextInput, BbTooltip } from 'bitboss-ui';
const keyName = ref<string | null>('billing-sync');
</script>
The classic pairing is an icon-only button. Keep its aria-label as well: the
label names the control, the tooltip describes it. Delete the label because "the
tooltip already says it" and the button becomes nameless for a screen reader.
Directive or component
When the label is plain text, use the directive. v-bb-tooltip attaches a
tooltip to any element with no wrapper and no slot, which is what makes it the
right tool inside a table.
<!-- string shorthand: the value is the label -->
<span v-bb-tooltip="'North Atlantic Treaty Organization'">NATO</span>
<!-- the directive argument sets the placement -->
<BbButton
v-bb-tooltip:bottom="'Saved automatically'"
aria-label="Saved"
icon="lucide:check"
/>
<!-- object form: a required text plus any BbTooltip prop -->
<span
v-bb-tooltip="{ text: 'Opens after 300ms', placement: 'right', delay: 300 }"
>Hover me</span>
<!-- an empty value attaches nothing, so it is safe to bind conditionally -->
<span v-bb-tooltip="isTruncated ? label : ''">{{ label }}</span>
- Q2 revenue review.pdf2.4 MB
- Supplier contract — Aeris.pdf795.8 KB
- Catalogue export 2026-08.csv144.8 KB
- Warehouse stock count.xlsx94.5 KB
- Product photography brief.docx42.1 KB
<template>
<ul class="grid max-w-xs gap-1">
<li
v-for="file in shown"
:key="file.id"
class="flex items-center gap-2 text-sm"
>
<BbIcon class="shrink-0 opacity-60" icon="lucide:file" size="sm" />
<!-- Object form: text plus any BbTooltip prop. An empty value attaches
nothing, so short names get no bubble at all. -->
<span
v-bb-tooltip="
file.name.length > 24 ? { text: file.name, delay: 400 } : ''
"
class="min-w-0 flex-1 truncate"
>
{{ file.name }}
</span>
<span class="shrink-0 text-xs opacity-60">
{{ formatFileSize(file.size) }}
</span>
</li>
</ul>
</template>
<script setup lang="ts">
import { BbIcon, vBbTooltip } from 'bitboss-ui';
import { files, formatFileSize } from '~/demo-data';
// A length threshold stands in for a real truncation test; in an app compare
// the element's `scrollWidth` with its `clientWidth`.
const shown = files.slice(0, 5);
</script>
Escalate to the component only when you need a slot: markup inside the label, or
the activator scope to make the trigger react. If it is a string, it is the
directive.
The directive runs on mount, so on a prerendered page its label reaches the DOM
only after hydration. The component with a text prop renders its description
into the server HTML.
The plugin registers the directive globally. Importing vBbTooltip in
<script setup> registers it locally instead, which is what the demos on this
site do.
Keyboard and assistive technology
Two rules. Break either one and nothing warns you: the page looks finished.
A tooltip needs a focusable trigger. It opens on hover and on
:focus-visible. That only helps if the element it hangs off can take focus. A
<span> cannot, so put the tooltip on a button, a link or a field. If the
trigger really has to be a span, give it a real role and tabindex="0".
A tooltip is never the only home for a fact. Hover text does not exist on a
touch screen. It does not print, and it is gone the moment the pointer moves.
Anything the user must know to fill the field in correctly belongs in
description or hint on the field itself. The tooltip carries the aside.
The truncated cell above is the honest exception: the full value is already a text node in the page, so a screen reader reads it either way.
Placement, timing and width
placement defaults to top and offers the top, right, bottom and left
families with -start and -end alignment.
<template>
<div
aria-label="Document actions"
class="inline-flex w-fit items-center gap-0.5 rounded-[var(--bb-radius)] border border-[color:var(--bb-border)] p-0.5"
role="toolbar"
>
<!-- `delay` holds each bubble back until the pointer settles, so crossing
the row does not fire five of them. `width` wraps the long one. -->
<BbTooltip
v-for="action in actions"
:key="action.label"
:delay="400"
placement="bottom"
:text="action.hint"
:width="action.wide ? 200 : undefined"
>
<template #activator="{ props }">
<BbButton
v-bind="props"
:aria-label="action.label"
:icon="action.icon"
size="xs"
variant="ghost"
/>
</template>
</BbTooltip>
</div>
</template>
<script setup lang="ts">
import { BbButton, BbTooltip } from 'bitboss-ui';
interface ToolbarAction {
label: string;
hint: string;
icon: string;
/** Long hints need a width cap or they stretch across the viewport. */
wide?: boolean;
}
const actions: ToolbarAction[] = [
{ label: 'Undo', hint: 'Undo', icon: 'lucide:undo-2' },
{ label: 'Redo', hint: 'Redo', icon: 'lucide:redo-2' },
{ label: 'Copy link', hint: 'Copy link', icon: 'lucide:link' },
{ label: 'Download PDF', hint: 'Download PDF', icon: 'lucide:download' },
{
label: 'Delete',
hint: 'Deletes the document for everyone it is shared with. This cannot be undone.',
icon: 'lucide:trash-2',
wide: true,
},
];
</script>
delay (ms, 0 by default) holds the opening back until the pointer settles.
On a dense toolbar that is the difference between a hint and five bubbles
flashing as the cursor crosses the row. It delays only the open, so it is not a
way to keep a tooltip on screen longer.
width caps the bubble in pixels, or as a percentage of the trigger, so a long
hint wraps instead of stretching across the viewport.
Labels with markup
The default slot takes markup, which is how an action name gets its keyboard shortcut beside it.
<template>
<div class="flex items-center gap-1">
<BbTooltip v-for="item in items" :key="item.label" :delay="200">
<template #activator="{ props }">
<BbButton
v-bind="props"
:aria-label="item.label"
:icon="item.icon"
size="sm"
variant="ghost"
/>
</template>
<!-- Markup, but still no links and no buttons: the bubble never takes
focus, so anything clickable in here is unreachable. -->
<span class="flex items-center gap-1.5">
{{ item.label }}
<kbd
class="rounded border border-current px-1 font-mono text-[10px] leading-4 opacity-75"
>
{{ item.keys }}
</kbd>
</span>
</BbTooltip>
</div>
</template>
<script setup lang="ts">
import { BbButton, BbTooltip } from 'bitboss-ui';
const items = [
{ label: 'Search', keys: '⌘K', icon: 'lucide:search' },
{ label: 'Toggle sidebar', keys: '⌘B', icon: 'lucide:panel-left' },
{ label: 'New document', keys: '⌘N', icon: 'lucide:square-pen' },
];
</script>
The content stays a description. Everything in the slot is flattened into one
string, so a screen reader hears "Search ⌘K" as a single phrase. The rule about
interactive content holds here too: markup does not make the bubble focusable,
and an <a> in there is unreachable.
Variants and styling
variant tints the bubble: default is the primary bubble and destructive
the danger tint. hide-arrow drops the point when it would land on a
neighbouring control.
<template>
<div class="flex flex-wrap items-center gap-2">
<BbTooltip text="Duplicate this record">
<template #activator="{ props }">
<BbButton v-bind="props" prepend:icon="lucide:copy" variant="outline">
Duplicate
</BbButton>
</template>
</BbTooltip>
<!-- `destructive` is the danger tint, for a hint that warns. -->
<BbTooltip text="Deleting removes every version of this record" variant="destructive">
<template #activator="{ props }">
<BbButton
v-bind="props"
prepend:icon="lucide:trash-2"
variant="destructive"
>
Delete
</BbButton>
</template>
</BbTooltip>
<!-- No arrow: a plain bubble, for a dense row where the point would land
on top of a neighbouring control. -->
<BbTooltip hide-arrow text="Visible to everyone with the link">
<template #activator="{ props }">
<BbButton v-bind="props" prepend:icon="lucide:share-2" variant="ghost">
Share
</BbButton>
</template>
</BbTooltip>
</div>
</template>
<script setup lang="ts">
import { BbButton, BbTooltip } from 'bitboss-ui';
</script>
variant accepts only registered names, so variant="info" does not compile
until info is in the plugin config:
// nuxt.config.ts
export default defineNuxtConfig({
bitboss: { tooltipVariants: ['info'] },
});
Registering a name gets you the type; the CSS is yours. Colour comes from three custom properties on the tooltip root, which the component re-applies to the bubble and the arrow:
.bb-tooltip.bb-tooltip--info {
--bg: var(--bb-panel);
--fg: var(--bb-text);
--border-color: var(--bb-border);
}
--fs, --px and --py size the text and its padding on the same root.
Derived tokens like --bb-text-muted keep the values they computed against the
page, not against the bubble. It is one more reason a tooltip holds text rather
than components.
Coming from v2
The v2 theme prop became variant, and it is typed.