Use it for
Reach for BbIndicator to pin a marker onto another element. An unread count on
a bell, a presence dot on an avatar, a New flag on a nav item.
Use something else when
BbBadge, when the marker is an inline label that belongs in the text, withclearablewhen it can be dismissed
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
Coming from v2
This component is what v2 called BbBadge. The port is mechanical: rename the
tag, move content to text and color to variant, drop floating. In CSS,
.bb-badge becomes .bb-indicator.
Unread counts
Put the element you are marking in the slot, never the other way round:
BbIndicator wraps it and pins text to its top-right corner.
<template>
<div class="flex flex-wrap items-center gap-4">
<!--
`count || undefined` is what hides the bubble at zero: passing 0 would
render the string "0", which is content like any other.
-->
<BbIndicator :max="99" :text="count || undefined" variant="destructive">
<BbButton icon="lucide:bell" variant="outline">Notifications</BbButton>
</BbIndicator>
<div class="flex flex-wrap items-center gap-2">
<BbButton size="sm" variant="secondary" @click="count += 1">
One more
</BbButton>
<BbButton size="sm" variant="secondary" @click="count += 40">
Forty more
</BbButton>
<BbButton size="sm" variant="ghost" @click="count = 0">
Mark all read
</BbButton>
</div>
<span class="text-sm opacity-70">{{ status }}</span>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { BbButton, BbIndicator } from 'bitboss-ui';
const count = ref(3);
const status = computed(() =>
count.value
? `${count.value} unread notification${count.value === 1 ? '' : 's'}`
: 'Inbox clear'
);
</script>
The indicator is inert. When the marker has to be clickable, put a BbButton in
the slot.
Presence dots
dot replaces the bubble's content with a plain filled marker, the right shape
for the state of a person or a service.
Marta Villoresi
Online
Lukas Brandt
Away
Sofia Marchetti
In a call
<template>
<div class="flex flex-wrap items-center gap-6">
<div v-for="row in roster" :key="row.user.id" class="flex items-center gap-2">
<BbIndicator dot :variant="row.variant">
<BbAvatar :alt="row.user.fullName" size="40">
{{ row.user.initials }}
</BbAvatar>
</BbIndicator>
<div class="text-sm">
<p class="font-medium">{{ row.user.fullName }}</p>
<p class="text-xs opacity-70">{{ row.presence }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { BbAvatar, BbIndicator } from 'bitboss-ui';
import type { IndicatorVariantType } from 'bitboss-ui';
import { users } from '~/demo-data';
// A dot carries no content, so presence lives in its color — and in the text
// beside it, because a color on its own is not a label.
const presences: { presence: string; variant: IndicatorVariantType }[] = [
{ presence: 'Online', variant: 'success' },
{ presence: 'Away', variant: 'warning' },
{ presence: 'In a call', variant: 'destructive' },
];
const fallback = { presence: 'Online', variant: 'success' } as const;
const roster = users
.slice(0, 3)
.map((user, index) => ({ user, ...(presences.at(index) ?? fallback) }));
</script>
It wins over text rather than combining with it. And a color on its own is not
a label: write the state beside it, as the example does.
Placement, color and size
bottom and left combine to move the marker to any of the four corners.
<template>
<div class="flex flex-wrap items-end gap-8">
<div class="flex flex-col items-center gap-2">
<BbIndicator :text="3" variant="destructive">
<BbButton icon="lucide:bell" variant="outline">Notifications</BbButton>
</BbIndicator>
<span class="text-xs opacity-70">top right · count</span>
</div>
<div class="flex flex-col items-center gap-2">
<BbIndicator bottom dot left size="sm" variant="success">
<BbAvatar alt="Ada Lovelace" size="40">AL</BbAvatar>
</BbIndicator>
<span class="text-xs opacity-70">bottom left · presence</span>
</div>
<div class="flex flex-col items-center gap-2">
<BbIndicator :text="9" size="xl" variant="warning">
<BbButton icon="lucide:inbox" size="xl" variant="outline"
>Inbox</BbButton
>
</BbIndicator>
<span class="text-xs opacity-70">large · warning</span>
</div>
</div>
</template>
<script setup lang="ts">
import { BbAvatar, BbButton, BbIndicator } from 'bitboss-ui';
</script>
The bottom suits a presence dot, clear of a face or initials; the top suits a count, clear of a button's label.
Status colors
variant maps the shared status colors: default, success, info,
warning, destructive.
Red on a count means "needs attention". If every marker in the app is
destructive, none of them is.
Sizes
Sizes run from xs to 2xl, md is the default.
Pick the size from the element you are decorating, not from the text in the bubble. A 56px avatar and a toolbar button do not take the same marker.
Counts that change
Bind the count you already keep and cap it with max: above the threshold the
bubble renders ${max}+.
Zero shows, so hide the marker by withholding text altogether:
:text="count || undefined". The bubble is a live region and announces the new
count on its own, so wrap a control that already has an accessible name.
Custom colors
Register a name in the plugin config for a color that recurs, and it becomes a
typed value of variant.
// nuxt.config.ts
export default defineNuxtConfig({
bitboss: { indicatorVariants: ['brand'] },
});
The library ships no CSS for registered variants. You style
.bb-indicator--brand yourself, setting the two custom properties the built-in
variants drive.
.bb-indicator.bb-indicator--brand {
--bg: #7c3aed;
--color: #fff;
}
For a one-off, override the same properties inline:
:style="{ '--bg': '#0891b2', '--color': '#fff' }".