Use it for
Reach for BbIcon when the icon is part of the design: a status in a table
cell, empty-state art, a list marker.
Also see: Icons.
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.
Draw an icon
Pass the icon name to icon. It is the only required prop.
- Same-day dispatchOrders placed before 2 pm leave the warehouse today.
- Stock alertsGet an email when a product drops below its reorder level.
- Verified suppliersEvery supplier is audited once a year.
<template>
<ul class="flex max-w-sm flex-col gap-3 text-sm">
<li v-for="feature in features" :key="feature.title" class="flex gap-3">
<BbIcon class="shrink-0" :icon="feature.icon" />
<span class="flex flex-col">
<span class="font-medium">{{ feature.title }}</span>
<span class="text-[color:var(--bb-text-muted)]">{{
feature.text
}}</span>
</span>
</li>
</ul>
</template>
<script setup lang="ts">
import { BbIcon } from 'bitboss-ui';
const features = [
{
icon: 'lucide:rocket',
title: 'Same-day dispatch',
text: 'Orders placed before 2 pm leave the warehouse today.',
},
{
icon: 'lucide:bell',
title: 'Stock alerts',
text: 'Get an email when a product drops below its reorder level.',
},
{
icon: 'lucide:circle-check',
title: 'Verified suppliers',
text: 'Every supplier is audited once a year.',
},
];
</script>
Coming from v2type → icon
The prop was called type in v2. A BbIcon still on type has no icon, so it
throws at mount: [BbIcon] `icon` is required, with the rename in the message.
Pick a name that resolves
icon takes provider:icon, for example lucide:check. For your own SVGs use
local:my-icon (the local: prefix is optional but recommended).
Install the collections you want and the bitboss-ui plugin finds them (without
the plugin, a named icon throws Failed to load icons):
npm install -D @iconify-json/lucide
<template>
<div class="flex flex-wrap items-start gap-6">
<!-- A provider set. `lucide` is available because this project has
@iconify-json/lucide installed; nothing else was registered. -->
<figure class="flex flex-col items-center gap-1">
<BbIcon icon="lucide:heart" size="xl" />
<figcaption class="text-xs opacity-70">lucide:heart</figcaption>
</figure>
<!-- The same shape, drawn here: heart.svg inside the configured iconDir
becomes local:heart. -->
<figure class="flex flex-col items-center gap-1">
<BbIcon icon="local:heart" size="xl" />
<figcaption class="text-xs opacity-70">local:heart</figcaption>
</figure>
<!-- Brand art no UI set carries. This is what local: is for. -->
<figure class="flex flex-col items-center gap-1">
<BbIcon icon="local:whatsapp" size="xl" />
<figcaption class="text-xs opacity-70">local:whatsapp</figcaption>
</figure>
<!-- Unprefixed names resolve to local:, never to a provider set. Same
glyph as local:whatsapp — and no editor preview. -->
<figure class="flex flex-col items-center gap-1">
<BbIcon icon="whatsapp" size="xl" />
<figcaption class="text-xs opacity-70">whatsapp</figcaption>
</figure>
</div>
</template>
<script setup lang="ts">
import { BbIcon } from 'bitboss-ui';
</script>
A name that does not resolve draws a question mark in a circle and logs one
console error with the fix. The cause is a typo, a missing npm install, or a
name the build did not bundle. The rest of the page keeps working.
You can also import your own SVGs as strings and pass them straight to icon.
Use only markup you control: an SVG carrying event handlers or a <script> is
dropped with a warning, never cleaned.
<script setup lang="ts">
import mark from '~/assets/marks/partner.svg?raw';
</script>
<template>
<BbIcon :icon="mark" size="2xl" />
</template>
Pick a size
Sizes run from xs to 2xl, and md (24px) is the default. A number is pixels, and a CSS length such as 3rem works too.
<template>
<div class="flex flex-wrap items-end gap-6 text-xs">
<!-- The named scale: 12, 16, 24, 28, 36 and 40 px by default. -->
<figure
v-for="size in sizes"
:key="size"
class="flex flex-col items-center gap-2"
>
<BbIcon icon="lucide:rocket" :size="size" />
<figcaption class="font-mono text-[color:var(--bb-text-muted)]">
{{ size }}
</figcaption>
</figure>
<span class="h-10 w-px self-center bg-[color:var(--bb-border)]" />
<!-- Off the scale, when a layout needs one exact value. -->
<figure class="flex flex-col items-center gap-2">
<BbIcon icon="lucide:rocket" :size="20" />
<figcaption class="font-mono text-[color:var(--bb-text-muted)]">
20
</figcaption>
</figure>
<figure class="flex flex-col items-center gap-2">
<BbIcon icon="lucide:rocket" size="3rem" />
<figcaption class="font-mono text-[color:var(--bb-text-muted)]">
3rem
</figcaption>
</figure>
</div>
</template>
<script setup lang="ts">
import { BbIcon } from 'bitboss-ui';
const sizes = ['xs', 'sm', 'md', 'lg', 'xl', '2xl'] as const;
</script>
Coming from v2xxl → 2xl
The largest key is now 2xl.
Set the colour on the parent
Icons inherit the text colour wherever they sit.
- Payment declined
- Shipped this morning
- Not processed yet
<template>
<ul class="flex flex-col gap-2 text-sm">
<!--
The color is on the row, never on the glyph. Each icon paints in
currentColor, so it takes whatever its container is set to — which
is also why the same three icons work unchanged in dark mode.
-->
<li class="flex items-center gap-2 text-[color:var(--bb-danger)]">
<BbIcon icon="lucide:circle-x" size="sm" />
<span>Payment declined</span>
</li>
<li class="flex items-center gap-2 text-[color:var(--bb-primary)]">
<BbIcon icon="lucide:truck" size="sm" />
<span>Shipped this morning</span>
</li>
<li class="flex items-center gap-2 text-[color:var(--bb-text-muted)]">
<BbIcon icon="lucide:circle-dashed" size="sm" />
<span>Not processed yet</span>
</li>
</ul>
</template>
<script setup lang="ts">
import { BbIcon } from 'bitboss-ui';
</script>
Coming from v2color removed
color="#6b7280" becomes a class or a token on the parent. A leftover color
raises no error, only a development warning and a bitboss-ui check finding.
Inside BbButton, BbBadge or BbAlert there is nothing to do: the icon
already takes the right colour.
Accessible name
Add label when the icon is the only thing saying something: it gets
role="img" and that text as its name. Leave it off when the text is already
beside it, or screen readers read it twice.
- Two-factor authentication
- Billing
- Card ending 4242
<template>
<dl class="flex max-w-sm flex-col gap-2 text-sm">
<div class="flex items-center justify-between gap-3">
<dt>Two-factor authentication</dt>
<!--
The icon carries the value: nothing else on the row says the
feature is on. `label` gives it role="img" and an accessible
name, so it is announced as "Enabled".
-->
<dd class="text-[color:var(--bb-primary)]">
<BbIcon icon="lucide:shield-check" size="sm" label="Enabled" />
</dd>
</div>
<div class="flex items-center justify-between gap-3">
<dt>Billing</dt>
<!--
Here the text beside the glyph already says everything. No
`label`: the icon is hidden from assistive tech instead of
repeating the sentence next to it.
-->
<dd class="flex items-center gap-1.5">
<BbIcon icon="lucide:credit-card" size="sm" />
<span>Card ending 4242</span>
</dd>
</div>
</dl>
</template>
<script setup lang="ts">
import { BbIcon } from 'bitboss-ui';
</script>
label does not produce a tooltip, and a tooltip on a bare icon is
pointer-only: keyboard and touch never open it. An info icon with a tip is an
icon-only ghost button:
<BbButton variant="ghost" icon="lucide:info" v-bb-tooltip="'Prices include VAT'">About prices</BbButton>.
When you do not need it
Most components take the icon name on a prop and place it themselves.
Hand-placing a BbIcon inside them is the most common mistake with this
component.
<template>
<!--
Not one BbIcon on this page. Every glyph here arrives as a name on the
host's own prop, and the host places, sizes and colors it to match its
own size — which is the part you would have to rebuild by hand.
-->
<div
class="flex max-w-md flex-col gap-3 rounded-(--bb-radius) border border-(--bb-border) p-4"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-2">
<span class="text-sm font-medium">{{ order.reference }}</span>
<BbBadge variant="secondary" prepend:icon="lucide:credit-card">
Paid
</BbBadge>
</div>
<BbButton prepend:icon="lucide:plus">New order</BbButton>
</div>
<BbTextInput
id="icon-host-search"
v-model="query"
label="Search orders"
name="search"
placeholder="Order number or customer"
prepend:icon="lucide:search"
/>
<BbAlert
hide-close
icon="lucide:triangle-alert"
text="Two orders are waiting for payment."
variant="warning"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbAlert, BbBadge, BbButton, BbTextInput } from 'bitboss-ui';
import { orders } from '~/demo-data';
const order = orders[0]!;
const query = ref<string | null>('');
</script>
The exceptions are components with no icon field, where you pass a BbIcon in a
slot: BbTabs, BbBreadcrumbs, BbRating, BbSwitch and BbAvatar.
- ORD-2026-0496Pending
- ORD-2026-0474Processing
- ORD-2026-0447Shipped
- ORD-2026-0417Delivered
- ORD-2026-0435Cancelled
- ORD-2026-0421Refunded
<template>
<ul class="flex max-w-sm flex-col gap-2 text-sm">
<li
v-for="order in rows"
:key="order.id"
class="flex items-center gap-2"
:class="STATUS_TONE[order.status]"
>
<BbIcon :icon="STATUS_ICON[order.status]" size="sm" />
<span class="min-w-0 flex-1 truncate font-medium">
{{ order.reference }}
</span>
<span class="shrink-0 text-xs">{{ orderStatusLabels[order.status] }}</span>
</li>
</ul>
</template>
<script setup lang="ts">
import { BbIcon, type IconType } from 'bitboss-ui';
import {
type OrderStatus,
orders,
orderStatusLabels,
orderStatuses,
} from '~/demo-data';
/**
* Every value is a complete literal. The plugin scans source files for
* `provider:name` strings and bundles exactly the ones it finds, so
* `lucide:${order.status}` would draw the missing-icon placeholder.
*/
const STATUS_ICON: Record<OrderStatus, IconType> = {
pending: 'lucide:clock',
processing: 'lucide:loader',
shipped: 'lucide:truck',
delivered: 'lucide:circle-check',
cancelled: 'lucide:ban',
refunded: 'lucide:rotate-ccw',
};
/** Color rides on the row, so the glyph follows through currentColor. */
const STATUS_TONE: Record<OrderStatus, string> = {
pending: 'text-[color:var(--bb-text-muted)]',
processing: 'text-[color:var(--bb-text-muted)]',
shipped: 'text-[color:var(--bb-primary)]',
delivered: 'text-[color:var(--bb-primary)]',
cancelled: 'text-[color:var(--bb-danger)]',
refunded: 'text-[color:var(--bb-danger)]',
};
/** One order per status, so every literal in the map is on screen. */
const rows = orderStatuses.map(
(status) => orders.find((order) => order.status === status)!
);
</script>