Most of a v2 → v3 upgrade is work the compiler does for you. With typed templates, renamed props, removed props and unregistered variants all fail loudly, component by component.
The rest needs care. A set of boolean defaults flipped, two visual defaults changed and several components behave differently, and neither the type-checker nor the console mentions any of it.
Do it in this order
The order matters. Steps 1 and 2 have to land before anything renders correctly, and step 3 has to land before a visual review means anything.
- Swap the setup. Install the build plugin and the runtime plugin, and
mount the one
<BbHost />. Nothing renders correctly until this is done. - Move your config. Everything you passed to
useBbConfig().setConfig()exceptlocalenow lives in the build-plugin options. - Re-base your theme. Token names and defaults changed, including the primary colour itself.
- Let the tool rewrite what it can, then fix the rest. Run
npx bitboss-ui check --fix(renames, polarity flips, value remaps and the item fields of dropdowns, tabs and breadcrumbs in.vuefiles). Then work through what it andvue-tscstill report, component by component. - Audit what the compiler cannot find. Untyped templates,
v-bindobjects and behavioural flips are silent. Read the behaviour list, then run the grep pass. - Convert class overrides to variants. A userland
class="bb-button--outline"keeps working, but the variant prop is typed and is the only form the registry validates.
After installing, restart the dev server and delete node_modules/.vite. A
server started before the install keeps serving the old pre-bundled JavaScript
next to the new CSS.
Already on a 3.0 beta? This page is not your path. Before installing the new version, see which of your files it breaks:
npm pack bitboss-ui@beta && tar -xzf bitboss-ui-*.tgz
npx bitboss-ui check --manifest package/dist/ai/components.json
After installing, list every breaking change since the version you had,
renamed CSS classes included. --since takes the version npm ls bitboss-ui
printed before the upgrade:
npx bitboss-ui changelog --since <old-version>
npx bitboss-ui check --fix then applies the recorded renames (props, slots,
pt keys, item fields, imports) and reports what is left for you.
The plugin pattern
v3 is a plugin, not a bag of exports. Two pieces are always required: a build plugin that generates the config, icon registry and typed variant lists, and a runtime plugin that injects styles and registers the directives. Installation covers the wiring for Vite, Nuxt and Inertia.
v2's <BbConfirm /> and <BbToast /> tags are gone. One
<BbHost /> at the app root renders every
confirm() and toast(), and it takes no props, so their settings move to the
plugin: the toast placement becomes toastPosition, a default width for
every confirm is confirm: { defaultSize }, and a pt you set on either tag
goes in the build-time pt map under BbConfirm or BbToast. confirm(), toast()
and the portal components keep their names. Without a host, a confirm()
resolves false after a second.
A hand-rolled provide('icons', …) is shadowed, not merged, by the plugin's own
registry, so icons registered that way stop resolving without an error. Move
them into iconDir, as Icons describes.
Config moves to build time
Everything that used to go through useBbConfig().setConfig() is now a
build-plugin option. Only locale stays runtime-editable, through
useBbConfig().
This is the change that makes the variant registries and the typed icon names possible: the plugin knows at build time what names exist, so an unregistered one is a compile error rather than an unstyled element.
Five defaults changed on the way, and none of them errors:
- The library's built-in strings (confirm buttons, empty states, close labels)
are English unless you set
locale. An Italian app setslocale: 'it'in the plugin. - The dialog and off-canvas size maps changed value. A
BbDialog size="md"was 652px wide in v2 and is 448px in v3. Pin the old widths if a layout relied on them:bitbossUi({ dialogDefaultSizes: { md: 652, lg: 896 }, offCanvasDefaultSizes: { md: 652, lg: 896 }, }); - Dialogs, popovers, dropdowns and selects open as bottom sheets on screens
narrower than 768px.
adaptive: falsein the plugin keeps them floating: a temporary crutch while the rest moves, removed in the adoption checklist's final phase. - Tooltips wait 100 ms on hover before opening (keyboard focus still opens
them at once).
:delay="0"restores the instant hover. - A popover leaves the page usable while it is open: an outside click closes
it and still reaches what was clicked. Set
modalon a popover that must block the page until it is dismissed.
Re-base your theme
v2 defaults
v3 defaults
<template>
<div class="grid gap-4 sm:grid-cols-2">
<!--
`bb-theme` re-derives hover, border and focus ring from the knobs set on
this element, so the v2 panel behaves like a whole v2 page would.
-->
<section
class="bb-theme grid content-start gap-3 rounded-[var(--bb-radius-surface)] border border-[var(--bb-border)] p-4"
:style="v2Defaults"
aria-labelledby="v2-defaults-title"
>
<p id="v2-defaults-title" class="m-0 text-sm font-medium">
v2 defaults
</p>
<BbTextInput id="v2-company" v-model="company" label="Company" name="v2_company" />
<BbButton class="justify-self-start">Save changes</BbButton>
</section>
<section
class="grid content-start gap-3 rounded-[var(--bb-radius-surface)] border border-[var(--bb-border)] p-4"
aria-labelledby="v3-defaults-title"
>
<p id="v3-defaults-title" class="m-0 text-sm font-medium">
v3 defaults
</p>
<BbTextInput id="v3-company" v-model="company" label="Company" name="v3_company" />
<BbButton class="justify-self-start">Save changes</BbButton>
</section>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbButton, BbTextInput } from 'bitboss-ui';
const company = ref<string | null>('Northwind Traders');
// The two values v2 shipped for the tokens that kept their name.
const v2Defaults = {
'--bb-primary': 'rgb(37 29 255)',
'--bb-primary-fg': '#ffffff',
'--bb-radius': '4px',
};
</script>
Six more tokens kept their name and changed value, so no rename map or grep finds them:
| Token | v2 | v3 |
|---|---|---|
--bb-input-h | 36px | 32px |
--bb-input-py | 4px | 3px |
--bb-input-prefix-w | 40px | 20px |
--bb-label-weight | 400 | 500 |
--bb-ring-size | 4px | 2px |
--bb-leading | 24px | 21px |
Keep a declaration only if you chose that value. Delete the ones that restated a v2 default: carried forward, they keep the v2 look.
The token layer also shrank, to 112 --bb-* tokens from around 140, and the old
--x-light / --x-dark / --x triple is gone. One name per token; .dark
re-declares only the knobs that change.
Re-declare your brand theme before you look at a single screen. Design tokens is the new architecture, and the theme builder is the fastest way to get a starting block.
Boolean props that flipped
The v3 rule: a boolean prop defaults to false, and its name states what
turning it on does. Props that defaulted to true were renamed to their
inverse.
So if you passed the old prop at its default (:show-close="true"), delete it.
If you passed the non-default (:show-close="false"), switch to the new name
(hide-close). A not-mobile value moves too:
allow-writing="not-mobile" becomes disable-writing="mobile".
| Component | v2 (default) → v3 |
|---|---|
BbAlert, BbDialog, BbOffCanvas | showClose (true) → hideClose, also in toast() options |
BbTable | allowSelectAll (true) → disableSelectAll |
BbTabs | animateX / animateY (true) → disableAnimateX / disableAnimateY |
BbButton | autoLoading (false) → disableAutoLoading: loading is now on |
BbDropdownButton | autoLoading (true) → disableAutoLoading |
BbDropdownButton | disabledWhileLoading (true) → removed: always disabled while loading |
BbSlider | thumbTranslate (true) → disableThumbTranslate |
BbColorPalette, BbSelectPopover | flip (true) → disableFlip |
BbSelect, BbSelectPopover | allowWriting (true) → disableWriting, default 'auto': no search field under six options |
BbDatePickerInput | allowWriting (true) → disableWriting |
BbCheckbox, BbRadio, BbSwitch | reverse (true) → reverse, meaning inverted |
BbAlert | ariaAtomic (true) → removed: the live region is fixed |
BbChip | clearable (true) → removed with the component; use BbBadge clearable |
This is a per-component rename, not a find-and-replace: where a component
dropped the feature, delete the prop. Neither
BbPopover nor
BbTooltip has a close button in v3, so
show-close is removed there and hide-close is not a valid prop on either.
npx bitboss-ui check catches both spellings.
Two of these change behaviour rather than only spelling.
BbButton now shows the loading spinner and disables
itself for an async @click handler; pass disable-auto-loading to keep the v2
behaviour. And reverse on BbCheckbox,
BbRadio and BbSwitch
means the opposite of what it did: unset renders identically, but a v2
:reverse="false" becomes a bare reverse, and a v2 reverse is deleted.
Variants became typed
In v2, theme took any string and a class override did the rest. In v3, variant
names are typed per family, and a name that is neither built-in nor registered
through the plugin fails to compile.
A theme value that did nothing in v2 may do something in v3, because v2
shipped no CSS for several of them. Before converting a value, grep your own
stylesheets for the matching modifier class: if nothing styled it, delete the
prop.
Registering a name is first-class and expected. See
Design language for the plugin
option and the stylesheet block that go with it. Both halves are required: a
registered name with no CSS is an unstyled pill, and CSS with no registration
fails vue-tsc.
Behaviour that changed
Nothing here fails to compile. Existing code runs and does something else, so read the list once against your screens.
- Selects no longer clear on a second click. Clicking the selected option of
a
BbSelectorBbSelectPopoverpicks it again and closes; it used to empty the field. Addclearableif your screens relied on click-to-clear. A single-choiceBbDropdowngroup follows the same rule: putclearable: trueon the group. - Short selects open without a search field. An array of fewer than six
options has none. Set
autoDisableWritingThreshold: 0in the plugin to keep every search field, and delete a:disable-writing="items.length < 6"you wrote by hand. prefill: falseis search-first. A provider-backed select with it fetches nothing until the user types. If you wanted fetch-on-open, delete the prop.confirm()closes itself. The promise settles as before, then the dialog disappears. A multi-step flow that kept it open passesautoClose: false.- Dynamic slot names are kebab-case.
#order_historyis#order-historyand#placedatfor aplacedAtkey is#placed-at. A stale name renders the fallback with no error;npx bitboss-ui check --fixrenames the ones whose keys sit in the same file. BbTableis a CSS grid. Selectors aimed attable,trortdmatch nothing, and#thead,#tbodyand#tfoottake<div role="row">rows. A column whosesortableis a backend sort key now shows the built-in sort button too.- A
nameis the field's path. A field joined to aBbFormvalidates, submits and receives schema errors under itsname, where v2 used the slugged label.@submitkeys anduseBbFormContext(id).valuesmove with it. - Form controls join the form they sit in. A filter or a "show advanced"
switch inside a
BbFormis now submitted with it. Give itformlessto keep it out. - A form locks while its save runs. When
@submitreturns a promise, every joined field is read-only and the submit button spins until it settles. Drop a:loadingyou bound by hand;disable-submit-lockon the form keeps the fields editable. - A failed submit moves focus to the first invalid field and emits
invalid. Delete code of yours that did the same. - A split button's main action is the component.
BbDropdownButtonno longer turnsitems[0]into its main button: that item is now the first menu row, and the button shows no label. Move it onto the component (label in the default slot,@click,href/to,type), and a<BbDropdownGroup>companion into#dropdown.npx bitboss-ui checkflags a split button with no label (BbDropdownButton). - Row clicks skip the controls inside the row. Ticking a row's checkbox or
clicking a link or button in a cell no longer emits
click:row. Delete the@click.stopandclosest()guards that worked around it. { label, value }items need noitem-textoritem-value. Object items with avaluekey and noitem-valuenow put that value inv-model, where they put the whole object.
Options and items covers the item shapes. The
package lists the rest in v2-to-v3.md, section 6.
The silent-failure audit
Everything the compiler cannot catch, in one pass. Start with
npx bitboss-ui check. Besides the markup in .vue and .md files, it reads
your stylesheets and <style> blocks: a selector on a .bb-* class v3 no
longer renders is reported (renamed-class names the successor), and so is a
custom property nothing reads (inert-token, inert-local).
What no tool reaches is composable option objects in .ts: toast({ … })
and confirm({ … }) inside stores, interceptors and helpers. Neither can a
check read a prop passed through a bound v-bind object. The grep pass covers
both. Run each line in your project; every hit needs a decision, not
necessarily a change.
# ── Props, slots and markup ──
# → queryKey, URL params move
rg -in 'querykey'
# → hide-close, or delete
rg -in 'show-?close'
# → disable-select-all
rg -in 'allow-?select-?all'
# BbButton: now on by default
rg -in 'auto-?loading'
# removed
rg -in 'disabled-?while-?loading'
# → disable-animate-x / -y
rg -in 'animate-?[xy]'
# → disable-thumb-translate
rg -in 'thumb-?translate'
# → disable-flip
rg -n ':flip=|flip="'
# removed, except on BbTooltip
rg -in 'hide-?arrow|show-?arrow'
# removed → BbBadge clearable
rg -n '<BbChip'
# v2 badges are v3 BbIndicator
rg -n '<BbBadge'
# → BbTabs
rg -n '<BbTab\b'
# BbTabs → #<value>:label
rg -n '#label[-:]'
# groups → legend-position
rg -in 'label-?position'
# → layout
rg -n ' direction='
# groups → list-layout
rg -in 'input-?direction'
# → 'interaction'
rg -n 'prefill="focus"'
# now search-first
rg -n ':prefill="false"'
# → disable-writing, inverted
rg -in 'allow-?writing'
# → string[] only
rg -in 'filter-?by'
# validateOn → validation-mode
rg -in 'validate-?on'
# → empty, empty-text
rg -in 'no-?data'
# → #prepend:outer
rg -n '#(prepend|append)-outer'
# removed slots
rg -n 'option:(prepend|append)|#chevron'
# removed props
rg -n 'show-?chevron|update-?on-?animation-?frame'
# underscore slot names and tab slugs → kebab-case
rg -n '#[a-z]+_[a-z0-9_]+|\?tab=[a-z0-9]+_'
# BbTooltip → delay (BbAvatar keeps timeout)
rg -n 'timeout' --type vue
# → icon=, and local: for your own icons
rg -nU '<BbIcon[^>]*type='
# BbTree ARIA roles are gone
rg -n 'role="tree|treeitem'
# removed color props
rg -n '<Bb(Checkbox|Radio|Switch|Slider|Rating|Avatar|Icon|Spinner)[^>]*color='
# providers now take ONE object { query, reason, modelValue, signal }
rg -nU ':items="[^"]*"|items: \w+'
# positional providers and prefill branches → reason !== 'search'
rg -n '\(query: string|\(prefill: boolean|if \(prefill'
# item.disabled → selectable
rg -n 'disabled:' --type ts --type vue
# object items with a `value` field and no item-value: v-model now holds that value
rg -n ':items=' --type vue
# → validation-label
rg -n '\bvid='
# checkbox/radio/switch: inverted
rg -n 'reverse'
# ── Composable options and imports (.ts / .js) ──
# toast(), confirm() → duration
rg -n 'timeout' --type ts --type js
# toast(), confirm() → variant
rg -n 'theme' --type ts --type js
# useToast() → dismiss()
rg -n 'dismissAll'
# confirm() now closes itself
rg -in 'auto-?close'
# → the yes / no configs
rg -n 'yesText|noText|onYes|onNo'
# shadowed by the plugin
rg -n "provide\('icons'"
# dropdown rows, tabs, crumbs: key → value, text → label; dropdown groups key → id
rg -n 'BbDropdownItem|BbTabsItem|BbBreadcrumbsItem' --type ts --type js
# only locale survives
rg -n 'setConfig'
# removed exports
rg -n 'IconRegistry|useQueue|useWizard'
The package carries the full list, token and class greps included, in
node_modules/bitboss-ui/dist/ai/guides/migration/v2-to-v3.md.
Where the rest lives
This page is the shape of the upgrade. The detail is on the component pages, where you will actually be standing when you hit it:
- Every component's Examples page carries its v2 notes inline, folded into a "Coming from v2" block beside the behaviour they concern.
- Every component's API page ends with a generated Renamed and removed list: the props, slots and methods that no longer exist, what replaces each one, and which ones fail silently.
- Forms. v2's validated wrappers are gone. The ordinary controls validate
inside a
BbForm, and v2'svid(the name shown in messages) becomesvalidation-label. Validated forms has the setup. - Class names. Coming from an earlier 3.0 beta rather than from v2, a
stylesheet that targets
.bb-*classes follows the renames innode_modules/bitboss-ui/dist/ai/guides/migration/v3-beta-css-classes.md, which carries a find-and-replace script.
Start from the component the compiler pointed at, not from here.
Once the compiler is clean, run npx bitboss-ui check and the grep pass once
more, then do the visual pass with the theme tokens in place. This order keeps
type errors, silent API changes and intentional visual changes as three
separate review steps. Then walk the
adoption checklist: green gates are not the
end of a migration while the app still builds by hand what the library ships.