Skip to content
bitboss-ui3.0.0-beta.64

Icons

How an icon name resolves, what ships, and which components already take an icon.

On this page

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

  1. 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.
  2. Keep local:* for your own artwork: logos, product illustrations, one-off drawings, as SVG files in iconDir (./assets/icons by default in Nuxt).
  3. 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.
  4. Write names as complete string literals. The bundle holds only the names the plugin finds written out in your project.
  5. Never hand-place BbIcon where 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:

bash
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.

wholesale
ShippedCancelled

Item config. BbDropdown entries, items and group headers alike, carry 'prepend:icon' and 'append:icon' fields. It is data, not markup:

ts
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>:label slot, or the generic label fallback.
  • BbBreadcrumbs: item:prepend and item:append around every item, or a named slot per item.value.
  • BbRating: the icon slot replaces each star, with checked, value and size in scope. Colour it with --accent on the root, not a class on the icon: a class freezes the hover preview and the error colour.
  • BbSwitch: the icon slot 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).

How was the delivery?

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:

  • xs12px
  • sm16px
  • md24px
  • lg28px
  • xl36px
  • 2xl40px
  • Payment declined
  • Not processed yet

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.

vue
<!-- 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.