Use it for
Use BbAvatar for a person's or entity's visual identity in member lists, activity feeds, and assignee cells. It is display-only.
Use something else when
BbIndicator, when status or a count belongs on the avatarBbDropdown, when the avatar opens an account menuBbIcon, when the mark represents no person or entity
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.
Show a member
Pass the photo and a meaningful alt; keep initials in the default slot so the same row survives a missing or failed image.

<template>
<div class="flex items-center gap-3">
<BbAvatar :alt="user.fullName" size="xl" :src="photo" :timeout="4000" />
<div class="flex flex-col text-sm">
<span class="font-medium">{{ user.fullName }}</span>
<span class="text-xs text-[color:var(--bb-text-muted)]">
{{ user.jobTitle }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { BbAvatar } from 'bitboss-ui';
import { usersWithPhoto } from '~/demo-data';
const user = usersWithPhoto[0]!;
/**
* A real portrait from a remote origin. Every photo demo on this page raises
* `timeout` past its 400ms default for that reason — see "Slow images and SSR".
*/
const photo = user.photo ?? undefined;
</script>
The fallback order is image, default slot, then the built-in person icon. On a fallback, alt becomes the accessible name. Leave it empty only when the avatar is decorative.
| xs | sm | md | lg | xl | 2xl | |
|---|---|---|---|---|---|---|
| Photo | ![]() | ![]() | ![]() | ![]() | ![]() | ![]() |
| Initials (slot) | IB | IB | IB | IB | IB | IB |
| No slot | ||||||
| Failed photo |
<template>
<div class="overflow-x-auto">
<table class="text-xs">
<thead>
<tr class="font-mono text-[color:var(--bb-text-muted)]">
<th class="pr-5 pb-2" />
<th v-for="size in sizes" :key="size" class="px-2 pb-2 font-normal">
{{ size }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.label">
<th
class="py-1.5 pr-5 text-left font-normal text-[color:var(--bb-text-muted)]"
>
{{ row.label }}
</th>
<td
v-for="size in sizes"
:key="size"
class="px-2 py-1.5 text-center align-middle"
>
<!-- A failed or missing source falls through to the slot. Any slot
content, even an empty string, replaces the person icon. -->
<BbAvatar
v-if="row.initials"
:alt="row.alt"
:size="size"
:src="row.src"
:timeout="4000"
>
{{ row.initials }}
</BbAvatar>
<BbAvatar
v-else
:alt="row.alt"
:size="size"
:src="row.src"
:timeout="4000"
/>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup lang="ts">
import { BbAvatar } from 'bitboss-ui';
import { users, usersWithPhoto } from '~/demo-data';
const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const;
const withPhoto = usersWithPhoto[0]!;
/** One of the many people in the directory who never uploaded a picture. */
const colleague = users.find((person) => person.photo === null)!;
/**
* Stands in for a broken URL: a data URI that decodes to nothing, so the
* browser fires `error` at once and the page makes no request.
*/
const brokenPhoto = 'data:image/png;base64,QUJD';
const rows = [
{
label: 'Photo',
alt: withPhoto.fullName,
src: withPhoto.photo ?? undefined,
},
{
label: 'Initials (slot)',
alt: colleague.fullName,
initials: colleague.initials,
},
{ label: 'No slot', alt: 'Unassigned' },
{
label: 'Failed photo',
alt: colleague.fullName,
src: brokenPhoto,
initials: colleague.initials,
},
];
</script>
size accepts the shared xs–2xl scale, pixels, or a CSS length. The image is always center-cropped into a circle; there is no square variant.
Presence and groups
Compose status and overflow around the avatar instead of turning the avatar into an interactive control.
Ingrid HalvorsenFinancial Analyst · Online
Chiara BonettiContent Strategist · Away- IBIdris BelloFrontend Engineer · Online
- RHRania HaddadSupport Manager · Away
<template>
<ul class="flex max-w-xs flex-col gap-2 text-sm">
<li
v-for="member in roster"
:key="member.id"
class="flex items-center gap-3"
>
<!--
BbIndicator positions and clips the dot. Never hand-place an
absolutely positioned marker over the avatar.
-->
<BbIndicator
bottom
dot
:variant="member.online ? 'success' : 'default'"
:style="member.online ? undefined : { '--bg': 'var(--bb-text-muted)' }"
>
<!-- A portrait where there is one, otherwise the initials fallback. -->
<BbAvatar
:alt="member.fullName"
size="40"
:src="member.photo ?? undefined"
:timeout="4000"
>
{{ member.initials }}
</BbAvatar>
</BbIndicator>
<span class="flex min-w-0 flex-col">
<span class="truncate font-medium">{{ member.fullName }}</span>
<!-- The dot is decorative and announced to nobody, so the state
is written out here as well. -->
<span class="truncate text-xs text-[color:var(--bb-text-muted)]">
{{ member.jobTitle }} · {{ member.online ? 'Online' : 'Away' }}
</span>
</span>
</li>
</ul>
</template>
<script setup lang="ts">
import { BbAvatar, BbIndicator } from 'bitboss-ui';
import { users } from '~/demo-data';
/** Presence is a fixed property of the fixture, never a random value. */
const roster = users
.slice(6, 10)
.map((user) => ({ ...user, online: user.id % 2 === 1 }));
</script>
- Engineering8 members


IB +4 - Design4 members

EYZA - Product3 members
NRMC
<template>
<ul class="flex max-w-md flex-col text-sm">
<li
v-for="team in teams"
:key="team.name"
class="flex items-center justify-between gap-4 border-t border-[color:var(--bb-border)] py-2.5 first:border-t-0"
>
<span class="flex flex-col">
<span class="font-medium">{{ team.name }}</span>
<span class="text-xs text-[color:var(--bb-text-muted)]">
{{ team.members.length }} members
</span>
</span>
<div class="avatar-stack">
<BbAvatar
v-for="(member, index) in team.members.slice(0, maxVisible)"
:key="member.id"
:alt="member.fullName"
size="36"
:src="member.photo ?? undefined"
:style="{ zIndex: maxVisible - index }"
:timeout="4000"
>
{{ member.initials }}
</BbAvatar>
<!-- The overflow marker is just another avatar: same size, same
ring, the count in the fallback slot. -->
<BbAvatar
v-if="team.members.length > maxVisible"
:alt="`${team.members.length - maxVisible} more members`"
size="36"
>
+{{ team.members.length - maxVisible }}
</BbAvatar>
</div>
</li>
</ul>
</template>
<script setup lang="ts">
import { BbAvatar } from 'bitboss-ui';
import { usersByTeam } from '~/demo-data';
const maxVisible = 4;
const teams = usersByTeam
.filter((group) => group.users.length > 1)
.slice(0, 3)
.map((group) => ({ name: group.label, members: group.users }));
</script>
<style scoped>
/*
* Stacking is the one case where reaching into the avatar root is the intended
* answer: the negative margin makes the overlap and the panel-colored ring
* keeps the overlapped edges readable. Earlier avatars sit on top, so each
* one loses only its leading edge and the initials stay whole. Keep it in one
* reusable class rather than as inline styles on each avatar.
*/
.avatar-stack {
display: flex;
}
.avatar-stack :deep(.bb-avatar) {
position: relative;
box-shadow: 0 0 0 2px var(--bb-panel);
}
.avatar-stack :deep(.bb-avatar + .bb-avatar) {
margin-inline-start: -5px;
}
</style>
A presence dot is silent, so repeat the status in text. Wrap a clickable avatar in a real button, link, or dropdown trigger; BbAvatar emits no events.
Slow images and SSR
The component checks the image timeout milliseconds after mount (400 by default) to recover events missed before hydration. A late image can still replace the fallback.
<BbAvatar :src="member.photo" :alt="member.fullName" :timeout="4000">
{{ member.initials }}
</BbAvatar>
Raise it for a slow origin, so readers do not see the initials flip to the photo. Use srcset with sizes for responsive sources; both pass through to the underlying image.
Bots and custom colour
For a bot or service account, place an unlabeled BbIcon in the fallback slot and keep the actor's name in alt. The fallback paints with --bb-primary and --bb-primary-fg; for a colour that comes from data, such as a workspace's own, set --bg and --fg on the avatar.
Release activity
- Release bot deployed v2.14.0 to production2 minutes ago
Lukas Brandt approved the release14 minutes ago- LOLumen Outdoor workspace switched to the new build1 hour ago
<template>
<section
aria-labelledby="release-feed-title"
class="flex max-w-md flex-col gap-3 rounded-(--bb-radius) border border-(--bb-border) p-4 text-sm"
>
<h3 id="release-feed-title" class="m-0 text-sm font-semibold">
Release activity
</h3>
<ul class="m-0 flex list-none flex-col gap-3 p-0">
<li v-for="entry in feed" :key="entry.id" class="flex items-center gap-3">
<!--
Bot: an unlabeled BbIcon in the fallback slot, the name in `alt`.
Workspace: its own color from data, set on the two locals.
-->
<BbAvatar
:alt="entry.actor"
size="36"
:src="entry.src"
:timeout="4000"
:style="
entry.tint && { '--bg': entry.tint.bg, '--fg': entry.tint.fg }
"
>
<BbIcon v-if="entry.bot" icon="lucide:bot" size="md" />
<template v-else>{{ entry.initials }}</template>
</BbAvatar>
<span class="flex min-w-0 flex-col">
<span class="truncate">
<span class="font-medium">{{ entry.actor }}</span>
{{ entry.action }}
</span>
<span class="text-xs text-(--bb-text-muted)">{{ entry.time }}</span>
</span>
</li>
</ul>
</section>
</template>
<script setup lang="ts">
import { BbAvatar, BbIcon } from 'bitboss-ui';
import { userById } from '~/demo-data';
const approver = userById[2]!;
type FeedEntry = {
id: number;
actor: string;
action: string;
time: string;
bot?: boolean;
src?: string;
initials?: string;
tint?: { bg: string; fg: string };
};
const feed: FeedEntry[] = [
{
id: 1,
actor: 'Release bot',
action: 'deployed v2.14.0 to production',
time: '2 minutes ago',
bot: true,
},
{
id: 2,
actor: approver.fullName,
action: 'approved the release',
time: '14 minutes ago',
src: approver.photo ?? undefined,
initials: approver.initials,
},
{
id: 3,
actor: 'Lumen Outdoor',
action: 'workspace switched to the new build',
time: '1 hour ago',
initials: 'LO',
// The color this workspace picked for itself, stored with the workspace.
tint: { bg: '#0f5132', fg: '#d1fae5' },
},
];
</script>
Coming from v2color → tokens
The color prop was removed. Retheme the primary pair for product-wide colour, or use --bg and --fg for one instance.