v3 is a plugin, not a bag of exports. You always install two pieces. The build plugin generates the config, the icon registry and the typed variant lists. The runtime plugin injects the styles and registers the directives. Without the build plugin the app does not build; without the runtime plugin components render unstyled.
Install the package
npm install bitboss-ui@beta
npm install -D @iconify-json/lucide
Confirm what you got before going further. @latest is the v2 line until
v3 goes stable.
# must report 3.0.0-*
npm ls bitboss-ui
Peers. Vue ^3.5.12 is the only required one. The optional ones you may
meet: @inertiajs/vue3 in Inertia apps, vee-validate (4.10 or later, below 5)
for BbForm, and @modelcontextprotocol/sdk plus
zod for the MCP server, which ai-init installs.
A build that meets BbForm without vee-validate stops and prints the install
command.
Node. 20.19 or later, or 22.12 or later on the 22 line. The bitboss-ui
command line needs the same.
Browsers. Evergreen only, with no polyfills. Every floating surface (select, dropdown, popover, tooltip) uses the native Popover API, so the floor is Chrome and Edge 114, Safari 17 and Firefox 125. Below it those surfaces do not render.
Upgrading. Restart the dev server after installing a new version, and
delete node_modules/.vite (inside the container under Docker or Sail). A
server left running keeps serving the old JavaScript next to the new CSS, which
looks like a regression; the console warns when the two disagree. The first
start prints the command that lists what broke since your previous version:
npx bitboss-ui changelog --since <previous-version>
The build plugin
Nuxt. The module wraps the Vite plugin and registers the runtime plugin for
you, so you write no app.use:
export default defineNuxtConfig({
modules: ['bitboss-ui/nuxt'],
});
Vite (Vue, Laravel with Inertia). Add the plugin, then install the runtime plugin yourself in the next section:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { bitbossUi } from 'bitboss-ui/vite';
export default defineConfig({
plugins: [vue(), bitbossUi()],
});
You configure everything here, at build time: the Nuxt bitboss key and
bitbossUi({ … }) take the same options. Two exceptions: locale
can change at runtime through useBbConfig(), and app.use(bitbossUiPlugin, { … })
takes locale and formControls per app.
The library's own strings (empty states, close buttons, pagination) are English
unless you set locale. An app in another language names it:
bitbossUi({ locale: 'it' }), or bitboss: { locale: 'it' } in Nuxt.
The plugin writes bitboss-ui.d.ts into your project the first time the dev
server starts or you build. It lands next to your first script entry
(resources/js/ in a Laravel app), else in src/, and in app/ under Nuxt 4.
The dts option moves it. The file types your custom variants, enabled locales
and passthrough scopes. Commit it, so a teammate's or CI's typecheck agrees
without running Vite.
Styles and the runtime plugin
The runtime plugin injects the library stylesheet for you. Do not also import
bitboss-ui/styles.css while injection is on. The second copy loads
unlayered and beats your Tailwind utilities again. npx bitboss-ui check
reports it as hand-imported-styles.
Vite. Install the runtime plugin at the entry point, with no CSS import:
import { createApp } from 'vue';
import { bitbossUiPlugin } from 'bitboss-ui';
import App from './App.vue';
createApp(App).use(bitbossUiPlugin).mount('#app');
In an Inertia app the same .use(bitbossUiPlugin) goes in createInertiaApp's
setup, after Inertia's own plugin (see Inertia).
Nuxt. The module installs the runtime plugin. The runtime plugin is also
what registers v-bb-tooltip, v-bb-dropdown, v-bb-color, v-bb-date,
v-bb-time, v-bb-hover-card, v-bb-popover, v-bb-dialog and
v-bb-off-canvas; without it those directives throw.
The host. confirm(), toast() and on-demand dialogs render in one
<BbHost />, mounted once at the app root beside the router outlet. It takes
no props and renders nothing on the server. In Nuxt it goes in app.vue, in
Inertia in the render root (see Inertia).
<template>
<RouterView />
<BbHost />
</template>
<script setup lang="ts">
import { BbHost } from 'bitboss-ui';
</script>
With no host, a confirm() resolves false after a second and toasts wait in a
queue. A second host renders nothing. A dev build warns about both.
Reset. The components expect a reset underneath them: sizes are written for
border-box, and native controls must arrive without browser chrome. Tailwind's
Preflight is one. If you run none, add the library's with resetCss: true. It
also sets a system-ui font stack on body at zero specificity, so any
font-family of yours wins. The library has no font token and inherits your
font, so set font-family on body either way.
Who wins a tie between your CSS and the library depends on how your CSS loads:
- Plain CSS: leave
cssLayersunset. Source order decides: the library loads first, so your CSS wins. - Tailwind v3: leave
cssLayersunset and turn injection off (below). The source order you set decides. - Tailwind v4: set
cssLayerstoTAILWIND_V4_LAYERS. Your utilities win, always. - Other layered CSS: set
cssLayersto your full layer order, with'bitboss-ui'in it. Your layer order decides.
With other layers, put 'bitboss-ui-reset' directly before 'bitboss-ui' when
resetCss is on.
Tailwind v4
Tailwind v4 puts its utilities in a cascade layer, and layered CSS loses to the
library's unlayered CSS on every property the library sets. px-6 or
rounded-lg on a BbButton then does nothing. Set cssLayers to slot the
library in below the utilities:
import { bitbossUi, TAILWIND_V4_LAYERS } from 'bitboss-ui/vite';
bitbossUi({ cssLayers: TAILWIND_V4_LAYERS });
In Nuxt the same line goes in the bitboss block, with the constant imported
from bitboss-ui/nuxt at the top of nuxt.config.ts. You add no @layer line to
your own CSS.
One rule stays yours to keep: your own element resets, such as
a { color: inherit }, beat the library too when they sit outside a layer.
Write them inside @layer base, where the library outranks them.
Tailwind v3
Preflight is ordinary unlayered CSS that loads after the injected library sheet, so its element resets win ties against the components. Turn injection off and import the sheet after your Tailwind entry:
bitbossUi({ injectStyles: false });
import './app.css';
import 'bitboss-ui/styles.css';
The library now also wins ties against your utilities. To have utilities win on
v3, wrap Tailwind's output in layers and list the same layers in cssLayers.
Both halves must agree. Injection goes back on, so drop injectStyles: false
and the bitboss-ui/styles.css import: the runtime plugin layers the sheet
itself.
@layer tailwind-base, bitboss-ui-reset, bitboss-ui, tailwind-utilities;
@layer tailwind-base {
@tailwind base;
}
@layer tailwind-utilities {
@tailwind components;
@tailwind utilities;
}
import { bitbossUi, TAILWIND_V3_LAYERS } from 'bitboss-ui/vite';
// ['tailwind-base', 'bitboss-ui-reset', 'bitboss-ui', 'tailwind-utilities']
bitbossUi({ cssLayers: TAILWIND_V3_LAYERS });
Use the constant rather than retyping the order: a list that drifts from the
@layer line loses every tie in silence. In Nuxt, import it from
bitboss-ui/nuxt.
On v3, also set passthroughDeduper: false, or point it at a module using
tailwind-merge@2: the bundled merger speaks Tailwind v4 class names.
Server rendering
Injection runs in the browser, so a server-rendered page needs the stylesheet from somewhere else, or it shows unstyled components until the JavaScript loads.
Nuxt: nothing to do. The module adds the stylesheet to the server render
itself, ahead of your own CSS and layered when cssLayers is set, and turns the
browser injection off.
Vite SSR and Inertia SSR with Tailwind v4: import the stylesheet into its layer from your CSS entry, and turn injection off.
@layer theme, base,
bitboss-ui-reset, bitboss-ui,
components, utilities;
@import 'tailwindcss';
@import 'bitboss-ui/styles.css' layer(bitboss-ui);
bitbossUi({ cssLayers: TAILWIND_V4_LAYERS, injectStyles: false });
The @layer line comes first: it fixes the order before Tailwind declares its
own layers, and with injection off nothing else declares the library's.
Nuxt without layers (plain CSS, Tailwind v3): list the stylesheet in css,
after your Tailwind entry, and turn injection off:
css: ['~/assets/css/main.css', 'bitboss-ui/styles.css'] with
bitboss: { injectStyles: false }.
Icons
Icon names are provider-first: lucide:plus resolves from the installed
@iconify-json/* package, and local:logo resolves from your own SVG directory.
iconDir defaults to ./assets/icons, resolved from the Vite root. In a
Vite app that is the project root, so icons under src/ need
iconDir: './src/assets/icons'. In Nuxt 4 the root is app/, so the default
already means app/assets/icons. Writing ./app/assets/icons points one level
too deep, and every local:* icon then renders as a question mark, in
production too.
Drop an SVG in that directory and it is available as local:<filename>.
Provider names built at runtime, which the build cannot see as literals, go in
iconWhitelist. Icons has the rest.
Options most apps set
The same options go in bitbossUi({ … }) and in Nuxt's bitboss key. The ones
most apps touch, by what they control:
Language and icons:
locale: the language of the library's own strings, and the formats around them. Default'en'. It is the one option you can change at runtime. See Locales.enabledLocales: which of the 38 built-in locales enter the bundle. Default: yourlocaleplus'en'.iconDir: the folder of SVG files forlocal:*icons, from the Vite root. Default'./assets/icons'.iconWhitelist: provider icons that always ship, for names built at runtime, such as'lucide:flag'or'tabler:calendar-*'.
Styles:
cssLayers: places the library in your cascade layer order,TAILWIND_V4_LAYERSfor Tailwind v4. Unset by default.injectStyles: injects the stylesheet in the browser. Defaulttrue; set itfalsewhen you import the stylesheet yourself.resetCss: adds the library reset,system-uibody font included. Defaultfalse.
Forms and selects:
formControls: what an emptiedBbTextInputorBbTextareaemits, and when fields in aBbFormvalidate. DefaulttextInputNullValue: 'null'('string'emits''),validationMode: 'balanced'.requiredAsterisk: a red*beside the label of required fields. Outside a form that is a field withrequired; in aBbFormit is a field whoserulesincluderequiredor whoseschemakey is required, or that carries therequiredhint. Defaulttrue;falsehides the marker everywhere.defaultInputLabelMode: where field labels sit,'outside'(the default),'floating'or'inside'.autoDisableWritingThreshold: a select with fewer options than this opens with no search field. Default6;0keeps the search field on every select.
Every instance of a component:
pt: the classes and attributes each instance starts from, keyed by component and part. See Passthrough.toastPosition: the corner toasts stack in, unless a toast names its own. Default'bottom-right'.confirm:{ defaultSize }, the maximum width of every confirm, from'xs'to'2xl', unless a call passes its ownsize. Unset leaves'sm'.
Phones and the server render:
adaptive: dialogs, popovers, dropdowns and selects open as bottom sheets belowmobileMaxWidth. Defaulttrue.mobileMaxWidth: the width in pixels below which the mobile layouts apply. Default768.ssrViewportWidth: the screen width the server renders for, since it cannot measure one. Default1024.
The dev server:
mcp: registers the MCP server in the project's agent configs when dev starts. Defaultfalse.vscodeSettings: writes editor hints to.vscode/settings.jsonwhen dev starts. Defaulttrue.
The options are typed, so your editor lists the rest. They are the variant
registries (see Design language),
injectDirectives, the size scales, dts, iconsScanner, passthroughDeduper
and the link settings (inertiaLinkName, nuxtLinkName, linkActiveClass).
The complete reference ships in the package, at
node_modules/bitboss-ui/dist/ai/guides/installation-and-plugin-setup.md.
formControls and confirm can also be set per app, merged key by key, with
app.use(bitbossUiPlugin, { formControls: { textInputNullValue: 'string' } }).
Locales
locale sets more than the words. It sets the formats around them: the long
date, the first day of the week, the order of a date field's parts, the AM / PM
words, and how a number field groups
what the user types.
The list is fixed and typed. A bare language is its default region: en is
en-US, it is it-IT, pt is pt-BR and zh is zh-CN.
| Language | Default | Regions, same words | Own dictionary |
|---|---|---|---|
| English | en (US) | en-GB, en-AU, en-CA, en-IE | |
| German | de | de-AT | de-CH |
| French | fr | fr-BE, fr-CA, fr-CH | |
| Italian | it | it-CH | |
| Dutch | nl | nl-BE | |
| Swedish | sv | sv-FI | |
| Spanish | es (Spain) | es-MX (Mexico) | |
| Portuguese | pt-BR (Brazil) | pt-PT (Portugal) | |
| Chinese | zh-CN (Simplified) | zh-TW (Taiwan), zh-HK (Hong Kong): Traditional | |
| One region each | cs da el fi hu ja ko nb pl ro ru tr uk |
A region reads its language's words and keeps its own formats: de-CH groups
1’234.5, en-GB starts the week on Monday and orders dates DD/MM/YYYY,
en-CA starts it on Sunday, and fr-CA orders them AAAA-MM-JJ. The English
regions spell the British way (colour), and de-CH writes ss for ß. Where the
words themselves differ, the region has a dictionary of its own: Mexican
Spanish, European Portuguese (its week starts on Sunday, as CLDR has it), and
Traditional Chinese for Taiwan and Hong Kong.
bitbossUi({
locale: 'en-GB',
enabledLocales: ['fr-CA', 'de-CH'],
});
pt-BRandzh-CNare the canonical spellings. Codes are case-insensitive, sozh-cnstill works, and a default region's full code is the same locale (it-ITisit).- A code that is not on the list is a type error. In a component,
npx bitboss-ui checkalso flags it and names the nearest one: forsetConfig({ locale: 'en-UK' })it asks whether you meanten-GB. - Selecting a locale you did not enable is a compile error, not a runtime
fallback.
enabledLocalesdecides what ships; at runtimeuseBbConfig().setConfig({ locale })switches among those only. There is deliberately no'all': each dictionary is a bundle cost you should see in the config. - There is no way to add your own. A language or region that is missing is a request for the library, not a dictionary in your app.
Imports and the type gate
Import components explicitly:
<script setup lang="ts">
import { BbButton, BbSelect } from 'bitboss-ui';
</script>
The Nuxt module can auto-import them instead, with autoImport: true in the
bitboss block. Leave it off, which is the default. Explicit imports are what
let the next setting catch a mistyped or removed component at type-check time:
{
"vueCompilerOptions": {
"checkUnknownComponents": true,
"strictTemplates": true
}
}
strictTemplates does the same for a misspelled prop. Run both through
vue-tsc 3. Under 2.x, the props of the generic components (BbSelect,
BbSelectPopover, BbTable, BbTabs, BbTabsRoot, BbTree, BbForm and
the checkbox, radio and switch groups) go unchecked while everything else still
errors, so the gate looks healthy. Keep TypeScript at 5.x or 6.x: vue-tsc
cannot load TypeScript 7 yet.
With auto-imports on, every Bb* tag resolves, so this check and the
bitboss-ui/no-unimported-component ESLint rule both go quiet.
Your first component
A select of team members, from the objects your API returns:
Stored value: 27
<template>
<div class="grid max-w-sm gap-3">
<BbSelect
id="assignee"
v-model="assignee"
label="Assignee"
name="assignee"
item-text="fullName"
item-value="id"
:items="users"
clearable
/>
<p class="m-0 text-sm text-[var(--bb-text-muted)]">
Stored value: <code class="font-mono">{{ assignee ?? 'null' }}</code>
</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbSelect } from 'bitboss-ui';
// Domain objects as they come from your API: no mapping to { label, value }.
const users = [
{ id: 14, fullName: 'Ada Okonkwo', team: 'Platform' },
{ id: 27, fullName: 'Luca Ferrari', team: 'Billing' },
{ id: 31, fullName: 'Mei Tanaka', team: 'Support' },
{ id: 48, fullName: 'Tomás Herrera', team: 'Platform' },
];
const assignee = ref<number | null>(27);
</script>
label is required: it is the accessible name. Pass your domain objects to
items as they are, then name the display field (item-text) and the stored
value (item-value). You never map them to { label, value } pairs first.
Objects that already carry label and value need neither prop.
Where to go next
Choose the next page by task:
- Upgrading from 2.x: read Migrating from v2 before changing call sites. Several boolean defaults changed without a type error.
- Building with Nuxt: continue with the Nuxt module.
- Building with Laravel: continue with Inertia and Laravel.
- Loading choices from an API: start with Options and items, then Fetching data.
- Adding validation: wrap the fields in a
BbForm, as Validated forms shows. - Theming: generate the first CSS block in the theme builder, then use Design tokens to review each value.
- Using a coding agent: run the setup in AI agents, then add the markup check to CI.