Use it for
Reach for BbHoverCard to preview what a link points at without leaving the
page. The person behind a name, an event in a calendar, a commit in a feed.
Use something else when
BbTooltip: one line of text that names or explains the controlBbPopover: the user works in the panel, with a form, a picker or settingsBbDropdown: it is a list of actions
Put nothing in a hover card that the reader cannot reach another way. A touch screen never shows it, and a screen reader in browse mode may never open it.
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.
A preview behind a link
Put a link in the activator slot and spread its props onto it.
Deploy of invoice-export approved by Amara Okonkwo two hours ago.
<template>
<p class="max-w-md text-sm">
Deploy of <code>invoice-export</code> approved by
<BbHoverCard placement="bottom-start" :width="280">
<!-- A link: it can take focus, so the keyboard opens the card too. -->
<template #activator="{ props }">
<a
v-bind="props"
class="font-medium underline decoration-dotted underline-offset-2"
:href="`#people-${member.id}`"
>
{{ member.fullName }}
</a>
</template>
<!-- Everything here is also on the profile the link opens. -->
<div class="flex items-center gap-3">
<BbAvatar :alt="member.fullName" size="48" :src="member.photo ?? member.avatar" />
<div class="min-w-0">
<p class="m-0 truncate text-sm font-medium">{{ member.fullName }}</p>
<p class="m-0 truncate text-xs opacity-70">
{{ member.jobTitle }} · {{ member.team }}
</p>
</div>
</div>
<ul class="m-0 mt-3 grid list-none gap-1 p-0 text-xs opacity-70">
<li class="flex items-center gap-1.5">
<BbIcon icon="lucide:mail" size="xs" />
<span>{{ member.email }}</span>
</li>
<li class="flex items-center gap-1.5">
<BbIcon icon="lucide:calendar" size="xs" />
<span>Joined {{ member.joinedAt }}</span>
</li>
</ul>
</BbHoverCard>
two hours ago.
</p>
</template>
<script setup lang="ts">
import { BbAvatar, BbHoverCard, BbIcon } from 'bitboss-ui';
import { userById } from '~/demo-data';
const member = userById[4]!;
</script>
The activator must take focus, so a keyboard user can open the card too. A
<span> cannot; a link can, and it already goes where the card points.
Shortcuts in the card
A card may hold shortcuts, as long as each one also exists on the page the link opens.
<template>
<div class="grid w-full max-w-xs gap-1.5">
<BbHoverCard
v-for="event in events"
:key="event.id"
:offset="8"
placement="right-start"
:width="260"
>
<template #activator="{ props }">
<a
v-bind="props"
class="block rounded-(--bb-radius) border-l-4 border-(--bb-primary) bg-(--bb-muted) px-2 py-1 text-sm"
:href="`#event-${event.id}`"
>
{{ event.start }} · {{ event.title }}
</a>
</template>
<template #header>
<p class="m-0 text-sm font-medium">{{ event.title }}</p>
<p class="m-0 text-xs opacity-70">
{{ event.day }}, {{ event.start }}–{{ event.end }}
</p>
</template>
<ul class="m-0 grid list-none gap-1 p-0 text-xs opacity-70">
<li class="flex items-center gap-1.5">
<BbIcon icon="lucide:map-pin" size="xs" />
<span>{{ event.room }}</span>
</li>
<li class="flex items-center gap-1.5">
<BbIcon icon="lucide:users" size="xs" />
<span>{{ event.people }}</span>
</li>
</ul>
<!-- Shortcuts only: the event page offers both actions too. -->
<template #footer>
<div class="flex gap-2">
<BbButton :href="`#event-${event.id}`" size="sm" variant="outline">
Open
</BbButton>
<BbButton :href="`#event-${event.id}-reschedule`" size="sm" variant="ghost">
Reschedule
</BbButton>
</div>
</template>
</BbHoverCard>
</div>
</template>
<script setup lang="ts">
import { BbButton, BbHoverCard, BbIcon } from 'bitboss-ui';
import { fixedTimes } from '~/demo-data';
const events = [
{
id: 'standup',
title: 'Team stand-up',
day: 'Wed 2 Sep',
start: fixedTimes.standup,
end: '09:45',
room: 'Room Brera',
people: '6 people',
},
{
id: 'design-review',
title: 'Design review',
day: 'Wed 2 Sep',
start: '11:00',
end: '12:00',
room: 'Video call',
people: 'Sofia Marchetti, Yuki Tanaka, Elif Yilmaz',
},
];
</script>
Tab from the activator moves into the card, and Tab past its last control moves
on and closes it. Escape closes it and puts focus back on the activator. The
header and footer slots stay outside the scrolling body, and every slot
gets close.
Timing and the safe zone
delay is how long the pointer or focus rests on the activator before the card opens: 350 ms by default.
Closing has no prop. Once the pointer has left both the activator and the card, the card waits 200 ms. A pointer heading diagonally for a button in the card stays inside a safe zone, so the card stays open. Try it on the events above.
A press on the activator, a right-click included, closes the card at once. When something the user opened on purpose is on screen, a menu or a dialog, the cards outside it stay closed. A click never toggles the card: the link does what links do.
To open or close the card from your own code, bind v-model. disabled
keeps it shut, and closes it if it is open.
One per row with v-bb-hover-card
For a card on every row, use v-bb-hover-card and write the body once in a
<BbHoverCardPortal>.
- Idris BelloDeveloper
- Rania HaddadAdmin
- Tomas DelgadoDeveloper
- Nikhil RaghavanAnalyst
- Elif YilmazDesigner
<template>
<ul class="m-0 grid w-full max-w-xs list-none divide-y divide-(--bb-border) p-0 text-sm">
<li
v-for="person in people"
:key="person.id"
class="flex items-center justify-between gap-2 py-1.5"
>
<!-- Object form: the portal's name, its data, and any BbHoverCard prop.
The argument is the placement. -->
<a
v-bb-hover-card:right="{
portal: 'person-card',
data: { person },
width: 240,
}"
class="underline decoration-dotted underline-offset-2"
:href="`#people-${person.id}`"
>
{{ person.fullName }}
</a>
<span class="text-xs opacity-70">{{ person.role }}</span>
</li>
</ul>
<!-- Declared once. It renders only inside the card that opens. -->
<BbHoverCardPortal v-slot="{ data }" name="person-card">
<div class="flex items-center gap-3">
<BbAvatar :alt="data.person.fullName" size="40" :src="data.person.avatar" />
<div class="min-w-0">
<p class="m-0 truncate text-sm font-medium">{{ data.person.fullName }}</p>
<p class="m-0 truncate text-xs opacity-70">
{{ data.person.jobTitle }} · {{ data.person.team }}
</p>
</div>
</div>
</BbHoverCardPortal>
</template>
<script setup lang="ts">
import { BbAvatar, BbHoverCardPortal, vBbHoverCard } from 'bitboss-ui';
import { users } from '~/demo-data';
const people = users.slice(8, 13);
</script>
No component exists until the pointer or focus reaches the row, so a thousand rows cost what a thousand links cost. The value takes three forms:
<!-- a portal name: mind the inner quotes -->
<a v-bb-hover-card="'person-card'" :href="person.url">Amara Okonkwo</a>
<!-- a component: the keys of data become its props -->
<a v-bb-hover-card="{ portal: PersonCard, data: { person } }" :href="person.url">
Amara Okonkwo
</a>
<!-- any BbHoverCard prop next to the portal; the argument is the placement -->
<a v-bb-hover-card:right="{ portal: 'person-card', data: { person }, width: 240 }" :href="person.url">
Amara Okonkwo
</a>
Without the inner quotes, person-card is person minus card, and nothing
opens. npx bitboss-ui check and the ESLint rule bitboss-ui/portal-names
catch that, and any name no <BbHoverCardPortal> declares. A named portal's
slot gets { data, close }, where data is the binding's data. A component
portal gets no close: when the content needs one, use a named portal.
Importing vBbHoverCard registers the directive locally, as these demos do; the
plugin's injectDirectives registers it globally.
Restyling with passthrough
Restyle the card through pt:<part>: root, panel, header, content and
footer, the same parts as BbPopover.
Fixed in 4f780d9 by Hugo Ferreira.
<template>
<p class="max-w-md text-sm">
Fixed in
<BbHoverCard
placement="bottom-start"
pt:content="text-xs leading-relaxed"
pt:header="bg-(--bb-muted) font-mono text-xs"
pt:panel="overflow-hidden"
:width="300"
>
<template #activator="{ props }">
<a
v-bind="props"
class="font-mono underline decoration-dotted underline-offset-2"
:href="`#commit-${commit.sha}`"
>
{{ commit.sha }}
</a>
</template>
<template #header>{{ commit.sha }} · {{ commit.branch }}</template>
{{ commit.message }}
</BbHoverCard>
by {{ commit.author }}.
</p>
</template>
<script setup lang="ts">
import { BbHoverCard } from 'bitboss-ui';
import { userById } from '~/demo-data';
const commit = {
sha: '4f780d9',
branch: 'main',
message: 'Round invoice totals after applying the discount, not before.',
author: userById[5]!.fullName,
};
</script>