Switch
An on/off toggle for instant, self-saving binary settings — built on Base UI Switch.
- Status
- Since
0.1.0- Accessibility pattern
- APG switch
Last updated
Install
Add Switch from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/switchUsage
import { Switch } from "@/components/ui/switch";
<Switch defaultChecked aria-label="Email notifications" />;Pair it with Field (horizontal orientation) for an associated, clickable label:
import { Field } from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";
<Field label="Email notifications" orientation="horizontal">
<Switch defaultChecked />
</Field>;Base UI renders the default switch root as a <span role="switch"> plus a hidden
<input>. When your markup needs a sibling <label htmlFor>, render the root as
a native button so the id targets the visible interactive element:
<label htmlFor="notifications-switch">Notifications</label>
<Switch
id="notifications-switch"
nativeButton
render={<button type="button" />}
/>Examples
Sizes
sm (16px), default (20px), and lg (24px) — the track and thumb scale together.
The grid shows every size in both the off and on positions, since the thumb-travel
geometry (the inset gap at rest vs. when checked) differs per size.
States
On, off, and disabled (in both positions). State is driven by Base UI's data-checked /
data-unchecked attributes, so it works controlled (checked + onCheckedChange) or uncontrolled
(defaultChecked).
Controlled
Drive the switch from state with checked + onCheckedChange. Use this when the value
must stay in sync with other UI or be persisted on change.
const [enabled, setEnabled] = useState(true);
<Switch
checked={enabled}
onCheckedChange={setEnabled}
aria-label="Email notifications"
/>;Notifications are on.
Invalid
The switch itself shows no visual invalid treatment — no destructive border and no status dot. It
keeps its normal appearance while aria-invalid is set (either directly on a standalone Switch, or
automatically when a wrapping <Field error=…> marks the control invalid), and the Field's error
copy is the affordance that explains what must be corrected.
Playground
Try every size and the disabled state, then copy the generated JSX.
<Field label="Email notifications" orientation="horizontal">
<Switch />
</Field>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
render | ComponentRenderFn<HTMLProps, SwitchRootState> | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | Replace the rendered track element via Base UI render composition. Pass a
ReactElement or a render function — Base UI merges this
wrapper's className, data-slot, and state data-* onto your element,
forwards the ref, and keeps the <Switch.Thumb> child. The element must
support role="switch" semantics. |
size | "lg" | "md" | "sm" | 'md' | Track + thumb scale. sm (16px), default (20px), lg (24px). |
Data attributes and CSS variables on Switch
| Attribute | Values |
|---|---|
data-size | mirrors a prop or state value |
data-slot | "switch" | "switch-thumb" |
Accessibility
- Renders a
<span role="switch">with a hidden<input>for form submission by default — exposesaria-checkedto assistive tech. UsenativeButton render={<button />}for sibling<label htmlFor>patterns. - Keyboard: Tab to focus, Space / Enter to toggle.
:focus-visibleshows a 2px ring (outline-ring) — neveroutline: none.disabledremoves it from the tab order and blocks toggling.- Always give the switch an accessible name — a
<Field>label, oraria-labelwhen standalone.
| Key | Action |
|---|---|
| Tab | Move focus to the switch |
| Space | Toggle the switch on / off |
| Enter | Toggle the switch on / off |
| Contract | States tested |
|---|---|
| Behaviour | default, checked, disabled, error, expanded, invalid, saving |
| Accessibility | focus-visible, invalid, labeled, semantic-html |
| Visual | default, disabled, invalid, checked, error |