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
- :has()
- Oklab and OkLCh — comes from the design tokens
- clip-path
- :focus-visible
- :where()
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
SwitchPropsSwitchInputPropsSwitchIndicatorPropsSwitchSizeSwitchVariant
Minimal Composition
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 surfaceSwitchInput: the nativerole='switch'checkbox — focus, keyboard, state and form valueSwitchIndicator: visible switch thumb and track state
Optional Parts
- None. The visible text goes outside the
Switchroot.
Composition Order
SwitchSwitchInputSwitchIndicator- a separate
LabelwithhtmlForpointing at theSwitchInputid, oraria-labelonSwitchInput
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
SwitchInputanamewhen 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,
Checkboxmay 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.
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.
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>
<SwitchInputForm/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.
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.
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.
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.
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.
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.
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
SwitchSwitchInputSwitchIndicator- a separate
LabelwithhtmlForpointing at theSwitchInputid, oraria-labelonSwitchInput
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.
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".