Skip to content

BbTooltip

A short label attached to a control, as a component or as the v-bb-tooltip directive.

import { BbTooltip } from 'bitboss-ui';

On this page

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 it
  • BbHoverCard: the hint previews what a link points at, such as a person or an event
  • BbDropdown: it is a menu of actions

A tooltip describes, a popover contains. The bubble never takes focus, so a link or a button inside it is unreachable from a keyboard.

Passthrough

Hover or tap a part to outline it. The toggles turn on loading, errors and warnings where the component has them; only parts get a row.

Hints on a control

Keep required guidance visible and use a tooltip for the aside. Put v-bb-tooltip on the control, with the label as its value.

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. Its three forms:

vue
<!-- string shorthand: the value is the label; an info tip is an icon-only ghost button -->
<BbButton variant="ghost" icon="lucide:info" v-bb-tooltip="'Prices include VAT'">About prices</BbButton>

<!-- 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 but `activator` (the directive uses its own element) -->
<BbButton
    v-bb-tooltip="{ text: 'Archive (E)', placement: 'right', delay: 300 }"
    aria-label="Archive"
    icon="lucide:archive"
/>

<!-- an empty value attaches nothing, so it is safe to bind conditionally;
     the one span here, because the truncated text is already in the page -->
<span v-bb-tooltip="isTruncated ? label : ''">{{ label }}</span>
AttachmentsINV-1042 · 4 files
  • Q3-2026 board deck, revenue and churn summary (final).key8.4 MB
  • invoice-INV-1042.pdf88 KB
  • customer-churn-analysis-2026-06-consolidated.xlsx1.2 MB
  • brand-guidelines.pdf640 KB

Escalate to the component only when you need a slot: markup inside the label, or the activator scope to make the trigger react. Pass the label to text, put the trigger in the activator slot and spread its props:

vue
<BbTooltip text="Download CSV">
    <template #activator="{ props }">
        <BbButton v-bind="props" aria-label="Download CSV" icon="lucide:download" />
    </template>
</BbTooltip>

If the label is a string, it is the directive.

When the trigger is not yours to wrap (an element another component renders, like the native input of a selection tile), pass it as activator instead: an element, a component ref or a CSS selector string, looked up once the tooltip mounts.

vue
<BbTooltip activator="#export-format-csv" text="Comma-separated, opens in any spreadsheet" />

Each element with the directive mounts one small BbTooltip as soon as it mounts, not on the first hover. That is what wires aria-describedby before anyone points at it, so a screen reader reads the label on first focus. A long table therefore mounts one per row: bind an empty value on the rows that do not need one.

The directive adds nothing to server-rendered HTML: it attaches on the client. The component with a text prop puts its description in the server HTML, which matters only when a crawler or a no-JS reader must get the text.

The plugin registers the directive globally (injectDirectives, on by default). Importing vBbTooltip in <script setup> registers it locally instead, which is what the demos on this site do. To register it by hand, or under another name, install BbTooltipDirectivePlugin:

ts
import { BbTooltipDirectivePlugin } from 'bitboss-ui';

app.use(BbTooltipDirectivePlugin); // v-bb-tooltip
app.use(BbTooltipDirectivePlugin, 'tooltip'); // v-tooltip (not typed)

Keyboard and assistive technology

A tooltip needs a focusable trigger. It opens on hover, after delay, and on :focus-visible, at once. That only helps if the element can take focus. A <span> cannot, so put the tooltip on a button, a link or a field. On a bare BbIcon, a <span> or an <svg> the tip is pointer-only: keyboard and touch never open it, so use that only when the tip repeats text already on the page. An info icon is an icon-only ghost BbButton, its slot text the name and the tip the description. Text that must be readable on touch belongs in a BbPopover toggletip. Never add a tabindex to an icon: it becomes a tab stop with no role.

The bubble is hidden from assistive technology. Its text reaches a screen reader as the trigger's description, once, and a tooltip inside a <label> does not join the control's name. In tests, assert the trigger's description (toHaveAccessibleDescription('…')) instead of looking the bubble up by role.

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 one 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.

delay (ms, 100 by default) holds the hover opening back until the pointer settles, so a cursor crossing a row of buttons does not flash a bubble for each. Once one tooltip has shown, a neighbour reached within 300 ms opens at once, and keyboard focus never waits; :delay="0" opens on hover at once too. It delays only the open, so it is not a way to keep a tooltip on screen longer.

Bubbles wrap at 40ch on their own. width sets a different maximum for one hint: pixels, a percentage of the trigger, or any CSS length. A short hint still shrinks to its text.

A tooltip gives way to anything the user opened on purpose. A click or right-click on the trigger hides it, and while a menu, popover, dialog, select or picker is open, tooltips on triggers outside it stay quiet. Nothing reshows when the overlay closes. disabled switches one off, and closes it if it is open.

Labels with markup

The default slot takes markup, which is how an action name gets its keyboard shortcut beside it.

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.

variant accepts only registered names, so variant="info" does not compile until info is in the plugin config:

ts
// 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:

css
.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 padding on the same root. To restyle one tooltip, pt:panel reaches the bubble and pt:root the floating element with its arrow; both take the open state.

Coming from v2

The v2 theme prop became variant, and it is typed. A hover now waits 100 ms before the bubble opens (focus still opens it at once); :delay="0" restores the instant hover.