Skip to content

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.

PreviewCode
RTL

Sizes

Three sizes for different contexts.

PreviewCode
RTL

States

Checked

Toggle between on and off states.

PreviewCode
RTL

Disabled

Prevent interaction and reduce opacity for unavailable options.

PreviewCode
RTL

Usage Examples

Form Integration

Switches work seamlessly with forms using name and value attributes.

PreviewCode
RTL

Event Handling

Listen to change events for custom logic.

PreviewCode
RTL

API Reference

Attributes

AttributeTypeDefaultDescription
checkedbooleanfalseSwitch checked state
disabledbooleanfalseDisable the switch
color'primary' | 'secondary' | 'success' | 'warning' | 'error''primary'Semantic color
size'sm' | 'md' | 'lg''md'Switch size
namestring-Form field name
valuestring-Form field value when on

Slots

SlotDescription
(default)Switch label content

Events

EventDetailDescription
change{ checked: boolean, value: string | null, originalEvent: Event }Emitted when checked state changes

CSS Custom Properties

PropertyDescriptionDefault
--switch-widthWidth of the switch trackSize-dependent
--switch-heightHeight of the switch trackSize-dependent
--switch-bgBackground when checkedColor-dependent
--switch-trackBackground of unchecked track--color-contrast-300
--switch-thumbBackground of the thumbwhite
--switch-font-sizeFont size of the labelSize-dependent

Accessibility

The switch component follows WCAG 2.1 Level AA standards.

bit-switch

Keyboard Navigation

  • Space / Enter toggle the switch.
  • Tab moves focus to and from the control.

Screen Readers

  • Uses role="switch" with aria-checked reflecting the on/off state ("true" or "false").
  • aria-labelledby links the label; aria-describedby links helper and error text.
  • aria-disabled reflects 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>