Skip to content

Migrating from v2

Upgrade in a safe order, handle inverted boolean defaults, and audit changes the compiler cannot find.

On this page

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.

  1. Swap the setup. Install the build plugin and the runtime plugin, and mount the one <BbHost />. Nothing renders correctly until this is done.
  2. Move your config. Everything you passed to useBbConfig().setConfig() except locale now lives in the build-plugin options.
  3. Re-base your theme. Token names and defaults changed, including the primary colour itself.
  4. 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 .vue files). Then work through what it and vue-tsc still report, component by component.
  5. Audit what the compiler cannot find. Untyped templates, v-bind objects and behavioural flips are silent. Read the behaviour list, then run the grep pass.
  6. 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:

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

bash
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 sets locale: '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:
    vite.config.ts
    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: false in 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 modal on a popover that must block the page until it is dismissed.

Re-base your theme

v2 defaults

v3 defaults

Six more tokens kept their name and changed value, so no rename map or grep finds them:

Tokenv2v3
--bb-input-h36px32px
--bb-input-py4px3px
--bb-input-prefix-w40px20px
--bb-label-weight400500
--bb-ring-size4px2px
--bb-leading24px21px

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

Componentv2 (default) → v3
BbAlert, BbDialog, BbOffCanvasshowClose (true) → hideClose, also in toast() options
BbTableallowSelectAll (true) → disableSelectAll
BbTabsanimateX / animateY (true) → disableAnimateX / disableAnimateY
BbButtonautoLoading (false) → disableAutoLoading: loading is now on
BbDropdownButtonautoLoading (true) → disableAutoLoading
BbDropdownButtondisabledWhileLoading (true) → removed: always disabled while loading
BbSliderthumbTranslate (true) → disableThumbTranslate
BbColorPalette, BbSelectPopoverflip (true) → disableFlip
BbSelect, BbSelectPopoverallowWriting (true) → disableWriting, default 'auto': no search field under six options
BbDatePickerInputallowWriting (true) → disableWriting
BbCheckbox, BbRadio, BbSwitchreverse (true) → reverse, meaning inverted
BbAlertariaAtomic (true) → removed: the live region is fixed
BbChipclearable (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 BbSelect or BbSelectPopover picks it again and closes; it used to empty the field. Add clearable if your screens relied on click-to-clear. A single-choice BbDropdown group follows the same rule: put clearable: true on the group.
  • Short selects open without a search field. An array of fewer than six options has none. Set autoDisableWritingThreshold: 0 in the plugin to keep every search field, and delete a :disable-writing="items.length < 6" you wrote by hand.
  • prefill: false is 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 passes autoClose: false.
  • Dynamic slot names are kebab-case. #order_history is #order-history and #placedat for a placedAt key is #placed-at. A stale name renders the fallback with no error; npx bitboss-ui check --fix renames the ones whose keys sit in the same file.
  • BbTable is a CSS grid. Selectors aimed at table, tr or td match nothing, and #thead, #tbody and #tfoot take <div role="row"> rows. A column whose sortable is a backend sort key now shows the built-in sort button too.
  • A name is the field's path. A field joined to a BbForm validates, submits and receives schema errors under its name, where v2 used the slugged label. @submit keys and useBbFormContext(id).values move with it.
  • Form controls join the form they sit in. A filter or a "show advanced" switch inside a BbForm is now submitted with it. Give it formless to keep it out.
  • A form locks while its save runs. When @submit returns a promise, every joined field is read-only and the submit button spins until it settles. Drop a :loading you bound by hand; disable-submit-lock on 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. BbDropdownButton no longer turns items[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 check flags 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.stop and closest() guards that worked around it.
  • { label, value } items need no item-text or item-value. Object items with a value key and no item-value now put that value in v-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.

bash
# ── 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's vid (the name shown in messages) becomes validation-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 in node_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.