Switch
A toggle switch component for binary on/off states. Perfect for settings, feature toggles, and preferences. Built with accessibility in mind and fully customizable through CSS custom properties.
Features
- ⚡ Touch-Optimized — 44 × 44 px minimum touch target for mobile
- 🌈 6 Semantic Colors — primary, secondary, info, success, warning, error
- 🎭 States — checked, unchecked, disabled
- 📏 3 Sizes — sm, md, lg
- 🔗 Form-Associated — participates in native form submission
- 🔧 Customizable — CSS custom properties for styling
Source Code
View Source Code
ts
import { defineComponent, html } from '@vielzeug/craftit';
import { useA11yControl, createCheckableControl } from '@vielzeug/craftit/labs';
import type { CheckableProps, DisablableProps, SizableProps, ThemableProps } from '../../types';
import { formControlMixins, sizeVariantMixin } from '../../styles';
import { useToggleField } from '../shared/composables';
import { SWITCH_SIZE_PRESET } from '../shared/design-presets';
import { mountFormContextSync } from '../shared/dom-sync';
import componentStyles from './switch.css?inline';
export type BitSwitchEvents = {
change: { checked: boolean; originalEvent?: Event; value: boolean };
};
export type BitSwitchProps = CheckableProps &
ThemableProps &
SizableProps &
DisablableProps & {
/** Error message (marks field as invalid) */
error?: string;
/** Helper text displayed below the switch */
helper?: string;
};
/**
* A toggle switch component for binary on/off states.
*
* @element bit-switch
*
* @attr {boolean} checked - Checked/on state
* @attr {boolean} disabled - Disable switch interaction
* @attr {string} value - Field value
* @attr {string} name - Form field name
* @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'
* @attr {string} size - Switch size: 'sm' | 'md' | 'lg'
* @attr {string} error - Error message (marks field as invalid)
* @attr {string} helper - Helper text displayed below the switch
*
* @fires change - Emitted when switch is toggled. detail: { value: boolean, checked: boolean, originalEvent?: Event }
*
* @slot - Switch label text
*
* @part switch - The switch wrapper element
* @part track - The switch track element
* @part thumb - The switch thumb element
* @part label - The label element
* @part helper-text - The helper/error text element
*/
export const SWITCH_TAG = defineComponent<BitSwitchProps, BitSwitchEvents>({
formAssociated: true,
props: {
checked: { default: false },
color: { default: undefined },
disabled: { default: false },
error: { default: '' },
helper: { default: '' },
name: { default: '' },
size: { default: undefined },
value: { default: 'on' },
},
setup({ emit, host, props, reflect }) {
const { checkedSignal, formCtx, triggerValidation } = useToggleField(props);
mountFormContextSync(host, formCtx, props);
// Pass writable checkedSignal directly — toggle() mutates it in place
const control = createCheckableControl({
checked: checkedSignal,
clearIndeterminateFirst: false,
disabled: props.disabled,
onToggle: (e) => {
triggerValidation('change');
emit('change', control.changePayload(e));
},
value: props.value,
});
const a11y = useA11yControl(host, {
checked: () => (control.checked.value ? 'true' : 'false'),
helperText: () => props.error.value || props.helper.value,
helperTone: () => (props.error.value ? 'error' : 'default'),
invalid: () => !!props.error.value,
role: 'switch',
});
reflect({
checked: () => control.checked.value,
classMap: () => ({
'is-checked': control.checked.value,
'is-disabled': !!props.disabled.value,
}),
onClick: (e: Event) => control.toggle(e),
onKeydown: (e: Event) => {
const ke = e as KeyboardEvent;
if (ke.key === ' ' || ke.key === 'Enter') {
ke.preventDefault();
control.toggle(e);
}
},
tabindex: () => (props.disabled.value ? undefined : 0),
});
return html`
<div class="switch-wrapper" part="switch">
<div class="switch-track" part="track">
<div class="switch-thumb" part="thumb"></div>
</div>
</div>
<span class="label" part="label" data-a11y-label id="${a11y.labelId}"><slot></slot></span>
<div
class="helper-text"
part="helper-text"
data-a11y-helper
id="${a11y.helperId}"
aria-live="polite"
hidden></div>
`;
},
styles: [...formControlMixins, sizeVariantMixin(SWITCH_SIZE_PRESET), componentStyles],
tag: 'bit-switch',
});Basic Usage
html
<bit-switch>Enable notifications</bit-switch>
<script type="module">
import '@vielzeug/buildit/switch';
</script>Visual Options
Colors
Six semantic colors for different contexts. Defaults to neutral when no color is specified.
Sizes
Three sizes for different contexts.
States
Checked
Toggle between on and off states.
Disabled
Prevent interaction and reduce opacity for unavailable options.
Usage Examples
Form Integration
Switches work seamlessly with forms using name and value attributes.
Event Handling
Listen to change events for custom logic.
API Reference
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
checked | boolean | false | Switch checked state |
disabled | boolean | false | Disable the switch |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'primary' | Semantic color |
size | 'sm' | 'md' | 'lg' | 'md' | Switch size |
name | string | - | Form field name |
value | string | - | Form field value when on |
Slots
| Slot | Description |
|---|---|
| (default) | Switch label content |
Events
| Event | Detail | Description |
|---|---|---|
change | { checked: boolean, value: string | null, originalEvent: Event } | Emitted when checked state changes |
CSS Custom Properties
| Property | Description | Default |
|---|---|---|
--switch-width | Width of the switch track | Size-dependent |
--switch-height | Height of the switch track | Size-dependent |
--switch-bg | Background when checked | Color-dependent |
--switch-track | Background of unchecked track | --color-contrast-300 |
--switch-thumb | Background of the thumb | white |
--switch-font-size | Font size of the label | Size-dependent |
Accessibility
The switch component follows WCAG 2.1 Level AA standards.
bit-switch
✅ Keyboard Navigation
Space/Entertoggle the switch.Tabmoves focus to and from the control.
✅ Screen Readers
- Uses
role="switch"witharia-checkedreflecting the on/off state ("true"or"false"). aria-labelledbylinks the label;aria-describedbylinks helper and error text.aria-disabledreflects the disabled state.- Minimum 44 × 44 px touch target for mobile.
Best Practices
Do:
- Use switches for instant actions that take effect immediately.
- Use clear labels that describe what the switch controls.
- Use appropriate colors (e.g., success for "enable", error for "disable critical feature").
Don't:
- Use switches when changes require a save/submit action (use checkbox instead).
- Use switches for more than two options (use radio buttons or select).
- Hide critical settings behind disabled switches without explanation.
When to Use Switch vs Checkbox
Use Switch for:
- Settings that take effect immediately
- Enabling/disabling features
- Toggling system states (on/off, true/false)
Use Checkbox for:
- Form selections that require submit
- Multiple selections in a list
- Agreeing to terms and conditions
Framework Examples
React
tsx
import '@vielzeug/buildit/switch';
function SettingsPanel() {
const [notifications, setNotifications] = useState(true);
return (
<bit-switch checked={notifications} onChange={(e) => setNotifications(e.detail.checked)}>
Enable notifications
</bit-switch>
);
}Vue
vue
<template>
<bit-switch :checked="notifications" @change="notifications = $event.detail.checked">
Enable notifications
</bit-switch>
</template>
<script setup>
import { ref } from 'vue';
import '@vielzeug/buildit/switch';
const notifications = ref(true);
</script>Svelte
svelte
<script>
import '@vielzeug/buildit/switch';
let notifications = true;
</script>
<bit-switch
checked={notifications}
on:change={(e) => notifications = e.detail.checked}
>
Enable notifications
</bit-switch>