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

BbSwitch

One on/off switch for the state of a thing, with your own values and a replaceable track.

import { BbSwitch } from 'bitboss-ui';

On this page

Use it for

Reach for BbSwitch when the control is the on/off state of a thing: dark mode, notifications, a beta flag, "anyone with the link can view".

Use something else when

  • BbCheckbox: it is an answer or a selection, like accepting terms or ticking "send me a copy"
  • BbSwitchGroup: several related on/off settings bound to one array

Pass Through

Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.

Guidance while the control is focused.

The commit model does not decide this. A switch is equally correct as a live toggle and as a field inside a form that commits on Save, and the library ships a validated BbSwitch from bitboss-ui/validated for exactly that.

The field states, the value contract and the layout are identical to BbCheckbox's, and are documented there. Carry one thing over: reverse inverted its meaning in v3, and the label-first settings row is where you will hit it.

Default

v-model holds the state and updates on every toggle, whether the user clicks the control or presses Space on it.

Sent on Monday mornings.

The control exposes role="switch" and its checked state on its own, so add no ARIA of your own. label is required and is the accessible name. hide-label keeps it for assistive technology only, for the rare row where adjacent text already names the switch.

Besides update:modelValue the switch re-emits the native events: change, click, input, focus, blur, keydown, mousedown, mouseup. Use them for analytics or side effects, never to work out the new value.

When the model is not a boolean

Point true-value and false-value at the two members of an enum and the model holds whichever the current state maps to.

Stored value: private

Do this whenever the API behind the switch speaks in names rather than booleans: a visibility setting, a plan tier, a retention mode. The translation layer you would otherwise write on both sides of the request stops existing.

The rest of the value contract is shared with BbCheckbox: any serializable value, structural equality, and submit-when-false plus name when a classic form post has to carry the unchecked case. See what the model holds.

Saving on toggle

A live switch, one with no Save button behind it, owns its own persistence, and the request can fail after the thumb has already moved.

Off

Bind :model-value and handle @update:model-value rather than using v-model, because you need to put the value back. Write it optimistically so the control responds at once, hold it readonly while the request is in flight so nobody queues conflicting toggles, and restore the previous value on failure.

In this demo turning it on succeeds and turning it off is refused, so flip it off to see the revert. In a real application pair the failure with useToast, because the user may not still be looking at that corner of the page.

Inside a submit-gated form, do none of this. Bind v-model and let the form commit.

What a surface must not do is both. A switch that already applied the moment it was flipped, sitting under a Save button that implies it had not, leaves the user unable to tell what is already live. Pick one commit model per surface.

A switch driven from outside

Two props paint the control from state you own elsewhere, without either of them entering the model.

Some channels are on and some are off.

checked forces the checked visual regardless of modelValue, and re-syncs whenever the prop changes. Interaction still emits update:modelValue for you to handle. Passing it together with a v-model gives the switch two sources of truth, and checked wins until you unset it.

indeterminate parks the thumb midway, for a master switch whose children disagree. It is a visual flag only: the model keeps its value, and the component re-asserts the state after clicks.

Coming from v2

checked used to accept the strings 'true' and 'false' as well. In v3 it is a plain boolean, so checked="false" no longer compiles. That string was truthy and turned the switch on, so this is a loud failure replacing a silent one.

Custom track and markup

There are two escape hatches here, and they are different sizes. Take the smaller one first.

The icon slot replaces the entire track and thumb, not a mark inside them, while keeping the input, the label, the field chrome and role="switch". It receives the live state: checked, focused, focusVisible, disabled, readonly, indeterminate, hasErrors, value, trueValue, falseValue and text.

Because the slot is the whole visual, what you put there has to communicate on, off and focused by itself. The common mistake is passing a bare icon and expecting the track to stay behind it: there is no track any more, so wrap the icon in a container you have styled.

When even the row is wrong, say a toolbar toggle or a compact inline control, BbBaseSwitchIcon gives you the track and thumb on their own.

BbBaseSwitchIcon has no input, no events and no state. It is aria-hidden and cannot be focused, so it must sit on a real control: a <label> wrapping an <input type="checkbox" role="switch"> hidden with sr-only, never with display: none. That role="switch" is what makes a screen reader say "on" and "off" rather than "checked". Pass focus-visible yourself, or a keyboard user gets no ring.

The custom properties live on the track element, so target that element rather than an ancestor. Change the geometry freely, because everything else derives from it: the thumb stays centred and travels flush at any size.

css
.bb-switch .bb-base-switch-icon {
    --w: 44px; /* track width, default 32px */
    --h: 24px; /* track height, default 18px */
    --thumb: 20px; /* thumb diameter, default 16px */
    --color: #16a34a; /* track fill when on */
    --radius: 6px; /* default 999px */
}

This is where v2's color prop went, removed with no replacement. Recolor through --color, or through the theme's --bb-primary when the whole product should follow.