Use it for
Use BbSwitchGroup for related states of things: storefront sections, feature flags, or notification channels.
Use something else when
BbCheckboxGroup— the rows are answers to a questionBbSwitch— there is one standalone stateBbRadioGroup— exactly one answer must be selected
Pass Through
Hover or tap a part to outline it. Toggles flip loading, errors and warnings when the component has them — only parts highlight.
The items, mapping, async provider, and coherence API is shared with BbCheckboxGroup. This page covers the switch-specific decisions.
Save settings on every flip
For a live settings panel, apply the next array optimistically, hold the group readonly during the request, and restore the previous array on failure.
Turn Lighting off to see a rejected save revert.
<template>
<div class="flex max-w-sm flex-col gap-3">
<!--
`:model-value` and `@update:model-value` instead of `v-model`, because
the handler has to be able to put the old value back. The group is
readonly while the request is in flight so nobody queues two flips.
-->
<BbSwitchGroup
id="storefront-sections"
:errors="error"
:items="productCategories"
legend="Sections visible in the storefront"
:model-value="visible"
name="section"
:readonly="saving"
@update:model-value="onSave"
/>
<p class="text-sm opacity-70" role="status">{{ status }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbSwitchGroup } from 'bitboss-ui';
import { delay, productCategories } from '~/demo-data';
const visible = ref<string[]>(['Furniture', 'Displays', 'Lighting']);
const saving = ref(false);
const error = ref<string>('');
const status = ref('Turn Lighting off to see a rejected save revert.');
const onSave = async (next: string[]) => {
const previous = visible.value;
// Optimistic: the switch moves now, and moves back only if the save fails.
visible.value = next;
saving.value = true;
error.value = '';
status.value = 'Saving…';
await delay(null, 700);
saving.value = false;
if (previous.includes('Lighting') && !next.includes('Lighting')) {
visible.value = previous;
error.value = 'Lighting has live campaigns and cannot be hidden.';
status.value = 'The save was refused and the switch went back.';
return;
}
status.value = `Saved: ${next.join(', ') || 'nothing visible'}.`;
};
</script>
Bind :model-value with @update:model-value when rollback matters. The event carries the whole next array. Inside a submit-gated form, use ordinary v-model and do not also save on every flip.
Explain unavailable switches
max disables off switches at the cap while leaving on switches available to trade. selectable gates specific items.
2 of 2 used
<template>
<div class="flex max-w-sm flex-col gap-3">
<!--
Two reasons a switch will not move, both resolved per option: `max`
holds the ones that are off once the cap is reached, `selectable`
rejects one outright.
-->
<BbSwitchGroup
id="alert-channels"
v-model="channels"
description="Two teams at a time. Finance alerts are set by your admin."
:items="teams"
legend="Escalate to"
:max="2"
name="escalate"
:selectable="notManaged"
/>
<p class="text-sm opacity-70">{{ channels.length }} of 2 used</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbSwitchGroup } from 'bitboss-ui';
import { teams } from '~/demo-data';
const notManaged = (team: string) => team !== 'Finance';
const channels = ref<string[]>(['Engineering', 'Support']);
</script>
State the rule in description; an unexplained switch that does not move looks broken.
Coming from v2
A disabled field on an item is ignored in v3. Move that condition into selectable. Use readonly to lock an existing selection; current disabled behavior can still allow selected rows to turn off.
Choose the model shape
Multiple mode is the default and requires a seeded array. :multiple="false" emits one bare value or null.
Model: 'IT'
<template>
<div class="flex max-w-sm flex-col gap-3">
<!--
`:multiple="false"` makes the group single-value: turning one on turns
the current one off, and turning the last one off emits null.
-->
<BbSwitchGroup
id="primary-residency"
v-model="residency"
description="Exactly one region holds the primary copy of your data."
item-text="name"
item-value="code"
:items="regions"
legend="Primary data residency"
:multiple="false"
name="residency"
/>
<p class="text-sm opacity-70">
Model:
<code>{{ residency === null ? 'null' : `'${residency}'` }}</code>
</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { BbSwitchGroup } from 'bitboss-ui';
import { countries } from '~/demo-data';
const regions = countries.slice(0, 3);
// A bare value, not an array of one. Seed it accordingly.
const residency = ref<string | null>('IT');
</script>
In v2, :multiple="false" was accepted but ignored. Audit existing models before upgrading because v3 now changes the emitted shape.
The group renders real switches with one required legend. Use stable primitive values through item-value, not whole domain objects.