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 { define, useField, html, inject, prop } from '@vielzeug/craft';

import type { CheckableProps, ComponentSize, ThemeColor } from '../../types';

import { type CheckableChangePayload, lifecycleSignal, createCheckable } from '../../headless';
import { disablableBundle, sizableBundle, SWITCH_SIZE_PRESET, themableBundle } from '../../shared';
import { colorThemeMixin, disabledStateMixin, forcedColorsFormControlMixin, sizeVariantMixin } from '../../styles';
import { applyCheckableBinding } from '../shared/field-binding';
import { FORM_CTX, useFormContext } from '../shared/form-context';
import { renderHelperRegion } from '../shared/templates';
import componentStyles from './switch.css?inline';

export type SgSwitchEvents = {
  change: CheckableChangePayload;
};

export type SgSwitchProps = CheckableProps & {
  /** Theme color */
  color?: ThemeColor;
  /** Disable interaction */
  disabled?: boolean;
  /** Error message (marks field as invalid) */
  error?: string;
  /** Helper text displayed below the switch */
  helper?: string;
  /** Component size */
  size?: ComponentSize;
};

/**
 * A toggle switch component for binary on/off states.
 *
 * @element sg-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: { checked: boolean, value: string, originalEvent?: Event }
 *
 * @slot - Switch label text
 *
 * @cssprop --switch-width - Track width
 * @cssprop --switch-height - Track height
 * @cssprop --switch-track-bg - Inactive track background color
 * @cssprop --switch-checked-bg - Active/checked track background color
 * @cssprop --switch-thumb-bg - Thumb background color
 * @cssprop --switch-font-size - Label font size
 * @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
 *
 * @example
 * ```html
 * <sg-switch name="notifications" checked color="primary">Enable notifications</sg-switch>
 * <sg-switch name="darkMode" size="sm">Dark mode</sg-switch>
 * <sg-switch disabled helper="Contact admin to change">Admin only</sg-switch>
 * ```
 */
export const SWITCH_TAG = 'sg-switch' as const;
define<SgSwitchProps, SgSwitchEvents>(SWITCH_TAG, {
  formAssociated: true,
  props: {
    ...themableBundle,
    ...sizableBundle,
    ...disablableBundle,
    checked: prop.bool(false),
    error: prop.string(),
    helper: prop.string(),
    name: prop.string(),
    value: prop.string('on'),
  },
  setup(props, { bind, emit, onCleanup }) {
    const formCtx = inject(FORM_CTX);
    const fCtxProps = useFormContext(bind, props, formCtx);

    let _formField: { reportValidity(): void } | null = null;
    const checkable = createCheckable({
      checked: props.checked,
      clearIndeterminateFirst: false,
      disabled: fCtxProps.disabled,
      error: props.error,
      getFormField: () => _formField,
      helper: props.helper,
      onToggle: (payload) => {
        checkable.triggerValidation('change');
        emit('change', payload);
      },
      prefix: 'switch',
      signal: lifecycleSignal(onCleanup),
      validateOn: formCtx?.validateOn,
      value: props.value,
    });

    _formField = useField<string | null>({
      disabled: checkable.disabled,
      toFormValue: (v) => v,
      value: checkable.checkableFormValue,
    });

    const { assistiveId, checked, disabled, errorText, handleClick, handleKeydown, helperText, labelId } = checkable;

    applyCheckableBinding(
      bind,
      fCtxProps.size,
      { assistiveId, checked, disabled, errorText, handleClick, handleKeydown, helperText, labelId },
      'switch',
    );

    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" id="${labelId}"><slot></slot></span>
      ${renderHelperRegion(assistiveId, errorText, helperText)}
    `;
  },
  styles: [
    colorThemeMixin,
    forcedColorsFormControlMixin,
    disabledStateMixin,
    sizeVariantMixin(SWITCH_SIZE_PRESET),
    componentStyles,
  ],
});

Basic Usage

html
<sg-switch>Enable notifications</sg-switch>

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-widthTrack widthSize-dependent
--switch-heightTrack heightSize-dependent
--switch-track-bgInactive (unchecked) track backgroundTheme-dependent
--switch-checked-bgActive (checked) track backgroundColor-dependent
--switch-thumb-bgThumb background colorTheme-dependent
--switch-font-sizeLabel font sizeSize-dependent

Accessibility

The switch component follows WCAG 2.1 Level AA standards.

sg-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/sigil/switch';

function SettingsPanel() {
  const [notifications, setNotifications] = useState(true);

  return (
    <sg-switch checked={notifications} onChange={(e) => setNotifications(e.detail.checked)}>
      Enable notifications
    </sg-switch>
  );
}

Vue

vue
<template>
  <sg-switch :checked="notifications" @change="notifications = $event.detail.checked"> Enable notifications </sg-switch>
</template>

<script setup>
import { ref } from 'vue';
import '@vielzeug/sigil/switch';

const notifications = ref(true);
</script>

Svelte

svelte
<script>
  import '@vielzeug/sigil/switch';
  let notifications = true;
</script>

<sg-switch
  checked={notifications}
  on:change={(e) => notifications = e.detail.checked}
>
  Enable notifications
</sg-switch>