Skip to content
v2.5.0
English
Русский
Documentation

Switch

Switch is a compound component for immediate binary settings, with a native input for semantics and state and an indicator for the visible track and thumb.
switch.tsx · 14 lines
import {
  Switch,
  SwitchIndicator,
  SwitchInput,
  Text,
} from '@vega-ui/react';

<div>
  <Text asChild size={3}><label htmlFor='preview-notifications'>Notifications</label></Text>
  <Switch>
    <SwitchInput id='preview-notifications' defaultChecked />
    <SwitchIndicator />
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 121
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
15 tests

Summary

Switch is a compound component for immediate binary settings, with a native input for semantics and state and an indicator for the visible track and thumb. It is the binary-toggle layer when the UI should read as on or off right now, not as one deferred form choice among others.

Imports

import {
  Switch,
  SwitchIndicator,
  SwitchInput,
  type SwitchProps,
  type SwitchSize,
  type SwitchVariant,
} from '@vega-ui/react';

Exported Types

  • SwitchProps
  • SwitchInputProps
  • SwitchIndicatorProps
  • SwitchSize
  • SwitchVariant

Minimal Composition

minimal-composition.tsx · 13 lines
import { Label, Switch, SwitchIndicator, SwitchInput } from '@vega-ui/react';

export default function MinimalComposition() {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)' }}>
      <Switch>
        <SwitchInput id='wifi' name='wifi' defaultChecked />
        <SwitchIndicator />
      </Switch>
      <Label htmlFor='wifi'>Wi-Fi</Label>
    </div>
  );

Required Parts

  • Switch: root track and context surface
  • SwitchInput: the native role='switch' checkbox — focus, keyboard, state and form value
  • SwitchIndicator: visible switch thumb and track state

Optional Parts

  • None. The visible text goes outside the Switch root.

Composition Order

  1. Switch
  2. SwitchInput
  3. SwitchIndicator
  4. a separate Label with htmlFor pointing at the SwitchInput id, or aria-label on SwitchInput

Variants

  • Sizes: sm, md, lg
  • Variants: primary, secondary
  • States: unchecked, checked, disabled

Edge Cases

  • Switches imply immediate binary state, not a deferred exclusive choice.
  • Give SwitchInput a name when the control belongs to a native form.
  • Labels should describe the controlled feature, not the toggle action itself.
  • If the binary choice is only one line inside a larger checklist or form submission flow, Checkbox may communicate intent better.

Common Mistakes

  • Using a switch for deferred form choices.
  • Writing labels like "Turn on" instead of naming the setting.
  • Leaving out SwitchInput, which is the actual control.

Examples

Basic

Basic: settings rows with descriptions

The Switch root is the track itself, so the visible title is a separate Label linked to SwitchInput with htmlFor/id, and the description is attached with aria-describedby. flex-shrink: 0 keeps the track from shrinking next to long text.

settings-toggle.tsx · 28 lines
import { Label, Switch, SwitchIndicator, SwitchInput, Text } from '@vega-ui/react';

const settings = [
  { id: 'auto-updates', title: 'Automatic updates', description: 'Install security patches overnight.', checked: true },
  { id: 'usage-data', title: 'Share usage data', description: 'Help us improve with anonymous diagnostics.', checked: false },
  { id: 'beta-features', title: 'Beta features', description: 'Try new tools before they are released.', checked: false },
];

export default function SettingsToggle() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      {settings.map((setting) => (

Controlled/Stateful

Controlled/Stateful: immediate effect

A switch applies its change right away. The parent owns checked, updates it from event.currentTarget.checked, and the status line below reflects the new state at once.

parent-owned-switch-state.tsx · 25 lines
import { useState } from 'react';
import { Label, Switch, SwitchIndicator, SwitchInput, Text } from '@vega-ui/react';

export default function ParentOwnedSwitchState() {
  const [maintenance, setMaintenance] = useState(false);

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-6)', width: '100%', maxWidth: 420 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>
        <Label htmlFor='maintenance-mode'>Maintenance mode</Label>
        <Switch>
          <SwitchInput

Form/Integration

Form/Integration: reading values with FormData

A switch submits like a checkbox: the name is present with the value on when it is on and absent when it is off, so read it with FormData.has. Reset restores defaultChecked.

form-data-submission.tsx · 45 lines
import { FormEvent, useState } from 'react';
import { Button, Fieldset, FieldsetLegend, Label, Switch, SwitchIndicator, SwitchInput, Text } from '@vega-ui/react';

const privacy = [
  { name: 'profile-public', title: 'Public profile', checked: true },
  { name: 'show-email', title: 'Show email address', checked: false },
  { name: 'search-indexing', title: 'Allow search engines to index my profile', checked: false },
];

export default function FormDataSubmission() {
  const [result, setResult] = useState('');

Layout/Overlay

Layout/Overlay: toggles inside a sheet

A short list of settings in a Sheet, named by its heading through aria-labelledby. The switches apply immediately, so the sheet needs no submit button, only a close button.

overlay-toggle-list.tsx · 60 lines
import { CSSProperties } from 'react';
import {
  Button,
  Heading,
  Icon,
  IconButton,
  Label,
  Sheet,
  SheetClose,
  SheetContent,
  SheetHandle,
  SheetTrigger,

Error

Error: rule across several switches

Turning both channels off breaks a product rule. The error appears right away, both inputs get aria-invalid and aria-describedby pointing at it, and turning either channel back on clears it. Switch has no invalid style of its own, so the message carries the state.

at-least-one-channel.tsx · 45 lines
import { useState } from 'react';
import { Fieldset, FieldsetLegend, HelperText, Label, Switch, SwitchIndicator, SwitchInput } from '@vega-ui/react';

export default function AtLeastOneChannel() {
  const [email, setEmail] = useState(true);
  const [push, setPush] = useState(false);

  const invalid = !email && !push;
  const describedBy = invalid ? 'channels-error' : undefined;

  return (
    <Fieldset style={{ width: '100%', maxWidth: 420 }}>

Disabled

Disabled: master switch disables a group

Turning off the master switch sets disabled on the Fieldset, and every switch inside inherits it natively. The security alerts switch is always disabled and explains why through aria-describedby.

disabled-switches.tsx · 48 lines
import { useState } from 'react';
import { Fieldset, FieldsetLegend, HelperText, Label, Switch, SwitchIndicator, SwitchInput } from '@vega-ui/react';

const kinds = [
  { id: 'notify-comments', title: 'Comments', checked: true },
  { id: 'notify-mentions', title: 'Mentions', checked: true },
  { id: 'notify-reviews', title: 'Review requests', checked: false },
];

export default function DisabledSwitches() {
  const [enabled, setEnabled] = useState(true);

Edge

Edge: saving to a server

The switch flips optimistically, then waits for the request. While saving it is aria-busy and aria-disabled instead of disabled, so focus stays on it; a failed request reverts the value and the reason is announced through aria-live. Turning on two-factor authentication fails on purpose.

async-toggle-with-pending.tsx · 65 lines
import { useState } from 'react';
import { Label, Switch, SwitchIndicator, SwitchInput, Text } from '@vega-ui/react';

const settings = [
  { id: 'weekly-digest', title: 'Weekly email digest' },
  { id: 'two-factor', title: 'Require two-factor authentication' },
];

const saveSetting = (id: string, value: boolean) => new Promise<void>((resolve, reject) => {
  setTimeout(() => {
    if (id === 'two-factor' && value) {
      reject(new Error('Add a verified phone number first.'));

Edge: right-to-left layout

Settings rows inside dir='rtl'. The row layout mirrors through flexbox; check that the thumb moves toward the inline end of the track when the switch turns on.

right-to-left.tsx · 22 lines
import { Label, Switch, SwitchIndicator, SwitchInput } from '@vega-ui/react';

const settings = [
  { id: 'rtl-dark-mode', title: 'الوضع الداكن', checked: true },
  { id: 'rtl-notifications', title: 'الإشعارات', checked: false },
];

export default function RightToLeft() {
  return (
    <div dir='rtl' lang='ar' style={{ display: 'grid', gap: 'var(--spacing-8)', width: '100%' }}>
      {settings.map((setting) => (
        <div key={setting.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 'var(--spacing-8)' }}>

Anatomy

Overview

Switch is a compound component for one immediate binary setting, with a native input for semantics and state and an indicator for the visible thumb and track.

Required Parts

Switch

Required. Owns size and variant context for the switch.

SwitchIndicator

Required. Renders the visible switch thumb and track surface.

SwitchInput

Required. The visually hidden native checkbox with role='switch': focus, keyboard, checked state and form value come from it.

Optional Parts

Switch has no optional parts. The Switch root is the track itself, so the visible text lives outside it and is linked with htmlFor.

Composition Order

  1. Switch
  2. SwitchInput
  3. SwitchIndicator
  4. a separate Label with htmlFor pointing at the SwitchInput id, or aria-label on SwitchInput

Valid Composition Patterns

Name the switch with a separate Label whose htmlFor matches the SwitchInput id, or, when there is no visible text, with aria-label on SwitchInput. Do not wrap Switch in another <label>: it already is one.

valid-composition-patterns.tsx · 19 lines
import { Label, Switch, SwitchIndicator, SwitchInput } from '@vega-ui/react';

export default function ValidCompositionPatterns() {
  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-8)' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--spacing-6)' }}>
        <Switch>
          <SwitchInput id='bluetooth' name='bluetooth' />
          <SwitchIndicator />
        </Switch>
        <Label htmlFor='bluetooth'>Bluetooth</Label>
      </div>

Invalid Composition Patterns

Switch used as a delayed submit choice

Switches imply immediate binary state, not a deferred form selection model.

Missing SwitchInput

The control may look correct but has no focusable input and does not participate in native submission.

Label describes the action instead of the state

Labels should describe the controlled feature, not "Turn on" or "Turn off".