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.
Five rules
- 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. - Never invent a name. Use only names that exist in an installed set or as
files in
iconDir. A missing icon throws at mount, and in dev that is a Vite error overlay with the exact fix in it. - Write names as static string literals. This one is not obvious and it fails in production only.
- 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.
In dev, all icons of the installed sets are registered, and a bad reference
fails loudly with an actionable message. You get either
"lucide:fo" requires "@iconify-json/lucide" — run: npm install -D … or
icon "x" does not exist in the lucide icon set.
In the build, .vue, .ts, .js, .tsx and .jsx sources are scanned,
tests and stories excluded, and each referenced icon is emitted as its own lazy
chunk. An icon used only in a .stories.ts or .test.ts therefore has to appear
as a literal in a scanned source file too.
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 the
main action, plus right:icon to replace the chevron in the split toggle.
<BbButton prepend:icon="lucide:plus">New invoice</BbButton>
<BbButton variant="outline" append:icon="lucide:download">Export</BbButton>
<BbButton variant="ghost" icon="lucide:settings">Settings</BbButton>
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.
<BbTextInput v-model="query" label="Search members" prepend:icon="lucide:search" />
<BbNumberInput v-model="amount" label="Amount" prepend:icon="lucide:euro" />
<BbDatePickerInput v-model="dueDate" label="Due date" prepend:icon="lucide:calendar" />
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.
Item config. BbDropdown entries, items and
group headers alike, carry 'prepend:icon' and 'append:icon' fields. It is
data, not markup:
const memberActions = [
{ key: 'edit', text: 'Edit member', 'prepend:icon': 'lucide:pencil' },
{ key: 'delete', text: 'Delete', variant: 'destructive', 'prepend:icon': 'lucide:trash-2' },
];
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-tablabel-{key}slot, or the genericlabelfallback.BbBreadcrumbs—item:prependanditem:appendaround every item, or a named slot peritem.key.BbRating— theiconslot replaces each star, withchecked,valueandsizein scope.BbSwitch— theiconslot replaces the track visual. Rarely needed; the default is right almost every time.BbDropdown—item:prepend/item:append, or per-entry${key}:prepend/${key}:append, when a leading region needs more than a named icon, such as an avatar.
<BbTabs v-model="tab" :items="tabs">
<template #label-billing="{ text }">
<BbIcon icon="lucide:file-text" size="sm" /> {{ text }}
</template>
</BbTabs>
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.
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.
<!-- Accessible name from the slot content, hidden visually. No aria-label needed. -->
<BbButton variant="ghost" icon="lucide:settings">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, keep every production icon name as a complete literal, and add local SVGs only for artwork the provider should not own. Then run the production build once. That build-time scan is the check that runtime-composed names cannot pass in development.