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

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

Also see: Validated forms.

The commit model does not decide this. A switch fits both a live toggle and a form that commits on Save: inside a BbForm it joins the form by name and submits with the rest.

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.

Passthrough

Hover or tap a part to outline it. The toggles turn on loading, errors and warnings where the component has them; only parts get a row.

Guidance while the control is focused.

A settings panel

Give each setting its own BbSwitch and set layout="label-fill" with reverse, so the label reads first and every switch lines up on the trailing edge.

NotificationsChanges apply as soon as you flip them.
PrivacyWho can see you and your activity.

When a setting only makes sense after another one, set disabled and say why in description. compact tightens the gap between each label and its description, which keeps a long panel dense. When a description runs to two lines, input-align="center" centres the switch on the label and description together instead of on the first line. If the rows are one array of enabled values rather than separate fields, use BbSwitchGroup instead.

The smallest switch is a label and a v-model, which updates on every toggle, whether the user clicks the control or presses Space on it. description is always-visible helper text; hint shows only while the switch has focus, unless you pin it with persistent-hint.

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.

States at a glance

Every field state a switch can show, next to each other.

Two-factor is required for admins.
Guests will lose access at midnight.

Lock an admin-managed setting with readonly, not disabled: it stays focusable and still submits. The difference between errors and warnings is explained on BbCheckbox.

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, compared structurally. When a classic form post must carry the unchecked case, set submit-when-false together with name. See what the model holds.

Saving on toggle or on submit

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.

Bind :model-value and handle @update:model-value rather than using v-model, because you need to put the value back. Write the value 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.

Joined to a BbForm, do none of this. The switch joins the form under its name, and the form submits it with everything else.

Submitted: nothing yet

A switch that only changes the view, like "Show advanced options", would join too and land in the submitted values. formless keeps it out. A field hidden with v-if leaves the values too, so "Expire the token" is not sent while the advanced options are closed. To keep its value, set keep-values-on-unmount on the form.

To make a joined switch mandatory, say an agreement that must be on, give it rules="required". The required prop is for a switch outside a form: on a joined one it is a hint that marks the switch and checks nothing. The asterisk follows the rule.

What a surface must not do is both. Take a switch that applied the moment it was flipped, under a Save button that implies it had not: the user cannot 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 current state: checked, focused, focusVisible, disabled, readonly, indeterminate, hasErrors, hasWarnings, 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.

To change only the label, say to append a BbBadge reading "Beta", fill the label slot instead. It receives text, and the accessible name stays.

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". The track cannot see the input's focus, so pass focus-visible yourself, as the demo does, 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 */
    --accent: #16a34a; /* track fill when on */
    --r: 6px; /* default 999px */
}

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