Icon names are provider-first. lucide:plus resolves from an installed
@iconify-json/* package, and local:logo from your own SVG directory. There is
no registration step and no per-icon component to import.
BbIcon documents the component. This page is the
policy around it, which is mostly about not reaching for it.
Rules for every icon
- Prefer provider icons. The house set is
lucide:*. Provider sets are plain npm packages you install yourself (npm install -D @iconify-json/lucide), and the plugin resolves them on its own. - Keep
local:*for your own artwork: logos, product illustrations, one-off drawings, as SVG files iniconDir(./assets/iconsby default in Nuxt). - Never invent a name. Use only names that exist in an installed set or as
files in
iconDir. A name that does not resolve draws a question-mark placeholder and logs one console error with the fix; in dev you also get the Vite error overlay. - Write names as complete string literals. The bundle holds only the names the plugin finds written out in your project.
- Never hand-place
BbIconwhere a component already accepts an icon through a prop, an item field, or a dedicated slot. The catalogue is further down.
How a name resolves
The bitbossUi Vite plugin, or the Nuxt module under the bitboss config key,
serves every icon through the virtual:bitboss-ui/icons module that the runtime
plugin hands to BbIcon.
Provider sets are auto-discovered. Every @iconify-json/* package in
node_modules is picked up. Install @iconify-json/lucide and every valid
lucide:<name> is available; remove it and they all become errors. There is
nothing to configure. To see which sets you already have, look for
@iconify-json/* in your package.json.
What ships. Every SVG in iconDir ships. From the provider sets, only the
names the plugin finds in your project, plus iconWhitelist. The scan reads
every file under the Vite root as plain text, PHP, Blade, JSON and Markdown
included, so a Laravel enum that returns 'tabler:user-share' counts. It skips
whatever .gitignore ignores, node_modules, vendor, storage, the build
output and hidden folders. iconsScanner.exclude and iconsScanner.include
adjust the list. Each name becomes its own lazy chunk.
Dev behaves like the build. The same scan runs when dev starts and again on every save. A name that would be missing in production is missing in dev too. Typing a new literal renders it without a restart.
A missing name is loud, never fatal. BbIcon draws the placeholder, tags it
data-bb-icon-missing="<name>" and logs one error per name. The message tells
you which fix it wants:
- does not exist in the lucide icon set, did you mean …? A typo: use the suggested name.
- exists in the lucide icon set but is not bundled. Write the name as a
literal, or add it to
iconWhitelist. - no installed icon set has the prefix "mdi". Install the set:
npm install -D @iconify-json/mdi.
To check a name before you use it, search the installed set:
node -p "
Object.keys(require('@iconify-json/lucide/icons.json').icons)
.filter((n) => n.includes('calendar'))
.join(' ')
"
Local icons: iconDir is scanned recursively, and each .svg becomes
local:<basename> with the extension dropped. A bare, unprefixed name normalises
to local:. Add or remove a file and dev picks it up live. The plugin also
maintains a collection JSON and the VS Code
iconify.customCollectionJsonPaths setting, so the Iconify extension previews
your local icons too.
Raw SVG is the escape hatch. BbIcon's icon prop also takes a full
<svg>…</svg> string, typically an asset imported with ?raw, for a one-off
drawing that lives in no collection.
Components that take an icon
Reach for these before BbIcon. Each one places, sizes and aligns the icon to
match its own size, which is work you would otherwise redo with CSS.
Buttons. BbButton takes prepend:icon and
append:icon beside the label; icon renders an icon-only button.
BbDropdownButton takes the same two on its
main button, which is the component itself: its label is the default slot.
toggle:icon replaces the chevron in the split toggle.
Input affixes. Every batteries-included input takes prepend:icon and
append:icon. The append side may already host clear, stepper or toggle
controls, so prefer prepend:icon when in doubt.
Status surfaces. BbAlert takes icon and
iconSize. BbToast takes icon in the useToast()
options, and its variants set the tone only, bringing no default icon.
BbBadge takes prepend:icon and append:icon, or
icon for an icon-only badge, in which case the other two are ignored.
BbTag takes the affix pair like the other inputs.
Buttons, inputs and status surfaces together. The source has no BbIcon: every
icon is a name handed to its host.
<template>
<div class="grid gap-6">
<!-- Buttons: an affix beside the label, or `icon` for an icon-only button.
BbDropdownButton is a button too: the label and the icon are its own. -->
<div class="flex flex-wrap items-center gap-2">
<BbButton prepend:icon="lucide:plus">New invoice</BbButton>
<BbDropdownButton :items="exportActions" prepend:icon="lucide:download" variant="outline">
Export
</BbDropdownButton>
<BbButton icon="lucide:settings" variant="ghost">Settings</BbButton>
</div>
<!-- Inputs: prefer `prepend:icon`; the append side hosts clear and toggles. -->
<div class="grid gap-3 sm:grid-cols-3">
<BbTextInput
id="icons-search"
v-model="search"
label="Search members"
name="search"
placeholder="Name or email"
prepend:icon="lucide:search"
/>
<BbNumberInput id="icons-amount" v-model="amount" label="Amount" name="amount" prepend:icon="lucide:euro" />
<BbTag id="icons-labels" v-model="labels" label="Labels" name="labels" prepend:icon="lucide:tag" />
</div>
<!-- Status surfaces: the badge and the alert place the icon themselves. -->
<div class="flex flex-wrap items-center gap-2">
<BbBadge prepend:icon="lucide:truck" variant="secondary">Shipped</BbBadge>
<BbBadge prepend:icon="lucide:ban" variant="destructive">Cancelled</BbBadge>
</div>
<BbAlert
hide-close
icon="lucide:triangle-alert"
text="Two products drop below their reorder point this week."
title="Low stock"
variant="warning"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import {
BbAlert,
BbBadge,
BbButton,
BbDropdownButton,
BbNumberInput,
BbTag,
BbTextInput,
type BbDropdownItem,
} from 'bitboss-ui';
const search = ref<string | null>('');
const amount = ref<number | null>(1250);
const labels = ref<string[]>(['wholesale']);
// The menu behind the chevron; Export is the component itself.
const exportActions: BbDropdownItem[] = [
{ value: 'csv', label: 'Export as CSV', 'prepend:icon': 'lucide:file-spreadsheet' },
{ value: 'pdf', label: 'Export as PDF', 'prepend:icon': 'lucide:file-text' },
];
</script>
Item config. BbDropdown entries, items and
group headers alike, carry 'prepend:icon' and 'append:icon' fields. It is
data, not markup:
const edit = {
value: 'edit',
label: 'Edit member',
'prepend:icon': 'lucide:pencil',
};
const remove = {
value: 'delete',
label: 'Delete',
variant: 'destructive',
'prepend:icon': 'lucide:trash-2',
};
const memberActions = [edit, remove];
append:icon is ignored when the item opens a submenu, which draws a chevron, or
sits in a selectable group, which draws a check. BbDropdownButton reuses the
same items shape, so its menu entries take icons the same way.
Components that take a slot
These surfaces take arbitrary content rather than a name, so here you do place a
BbIcon, or something richer, yourself.
BbTabs: the per-tab<value>:labelslot, or the genericlabelfallback.BbBreadcrumbs:item:prependanditem:appendaround every item, or a named slot peritem.value.BbRating: theiconslot replaces each star, withchecked,valueandsizein scope. Colour it with--accenton the root, not a class on the icon: a class freezes the hover preview and the error colour.BbSwitch: theiconslot replaces the track visual. Rarely needed; the default is right almost every time.BbDropdown:item:prepend/item:append, or per-entry<value>:prepend/<value>:append, when a leading region needs more than a named icon, such as an avatar. A slot replaces the icon and never sits beside it.
A per-item slot name uses the item's value in kebab-case: billingInfo becomes
#billing-info:label, placedAt becomes #placed-at:prepend. Both slots
below, live: a tab label with an icon, and hearts coloured through --accent
(hover them to see the preview follow).
<template>
<div class="grid gap-8">
<!-- The value is billingInfo; its slot name is kebab-case: #billing-info:label. -->
<BbTabs v-model="tab" :items="tabs">
<template #billing-info:label="{ label }">
<span class="inline-flex items-center gap-1.5">
<BbIcon icon="lucide:receipt" size="sm" />
{{ label }}
</span>
</template>
</BbTabs>
<!-- Colour on the root through --accent, so hover preview and the error tint keep working. -->
<BbRating
id="guide-icon-slots-rating"
v-model="score"
legend="How was the delivery?"
name="guide-icon-slots-rating"
pt:root="[--accent:var(--bb-danger)] [--empty:var(--bb-text-faint)] [--border-color:transparent]"
>
<!-- `checked` is true on every lit heart; the fill follows currentColor. -->
<template #icon="{ size, checked }">
<BbIcon :class="{ '[&_path]:fill-current': checked }" icon="lucide:heart" :size="size" />
</template>
</BbRating>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbIcon, BbRating, BbTabs } from 'bitboss-ui';
const tab = ref('profile');
const tabs = [
{ value: 'profile', label: 'Profile' },
{ value: 'billingInfo', label: 'Billing info' },
{ value: 'team', label: 'Team' },
];
const score = ref<number | null>(3);
</script>
Size and colour
Icons take the text colour of whatever they sit in. Colour the container,
never the icon markup. A color or fill on the icon element is the one thing
that breaks when a variant repaints around it.
BbIcon's size takes the xs to 2xl scale, which is 12, 16, 24, 28, 36 and
40 px by default. Retune the whole scale through the plugin's iconDefaultSizes
option. size also takes a number in pixels, or a CSS length such as
"1.5rem".
Icons you pass through props or item fields are sized and aligned by the host component to match its own size. Do not fight that with CSS. Change the host's size instead.
The scale, the same icon on three button sizes, and two rows that colour the container rather than the icon:
xs12pxsm16pxmd24pxlg28pxxl36px2xl40px
- Payment declined
- Not processed yet
<template>
<div class="grid gap-6">
<!-- The named scale, as the plugin ships it. `iconDefaultSizes` retunes it. -->
<ul class="m-0 grid max-w-md list-none grid-cols-6 items-end gap-2 p-0">
<li v-for="step in scale" :key="step.size" class="grid justify-items-center gap-1">
<BbIcon icon="lucide:package" :size="step.size" />
<code class="text-xs">{{ step.size }}</code>
<span class="text-xs opacity-60">{{ step.px }}px</span>
</li>
</ul>
<!-- An icon passed to a host follows the host's size: change `size` on the button, not the icon. -->
<div class="flex flex-wrap items-center gap-2">
<BbButton v-for="size in hostSizes" :key="size" prepend:icon="lucide:package" :size="size" variant="outline">
Size {{ size }}
</BbButton>
</div>
<!-- Colour lives on the container; the icon paints in currentColor. -->
<ul class="m-0 grid list-none gap-2 p-0 text-sm">
<li class="flex items-center gap-2 text-[color:var(--bb-danger)]">
<BbIcon icon="lucide:circle-x" size="sm" />
Payment declined
</li>
<li class="flex items-center gap-2 text-[color:var(--bb-text-muted)]">
<BbIcon icon="lucide:circle-dashed" size="sm" />
Not processed yet
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { BbButton, BbIcon } from 'bitboss-ui';
const scale = [
{ size: 'xs', px: 12 },
{ size: 'sm', px: 16 },
{ size: 'md', px: 24 },
{ size: 'lg', px: 28 },
{ size: 'xl', px: 36 },
{ size: '2xl', px: 40 },
] as const;
const hostSizes = ['sm', 'md', 'lg'] as const;
</script>
Naming an icon
Three cases, and only one of them takes a label.
Decorative, next to visible text, such as an affix, a button's prepend or a dropdown item. No label. The text already announces it, and a label would say it twice.
Meaningful and standalone, such as a status icon in a table cell with no
words beside it. Give BbIcon a label: it adds role="img" and aria-label.
An icon-only button. Supply the name through the label, which is the default
slot or text. It renders visually hidden but is still announced. aria-label
is only an override, not the way to name it. The one hard rule is that the button
must get its name from one of the two.
<!-- The hidden label names it. -->
<BbButton icon="lucide:settings" variant="ghost">
Settings
</BbButton>
Never wrap a BbIcon in a raw <button>. That is BbButton with icon and a
label, and it is shorter.
For a new project, install one provider set and write every icon name as a
complete literal. Put the few runtime names in iconWhitelist, and add local
SVGs only for artwork the provider should not own. A name that slips through
shows up as a placeholder in dev, before anyone deploys it.