Switch
Turns one setting on or off, and the change takes effect at once. Black when on, in two sizes, with an optional label that toggles it too. Built on the vendored Base UI switch.
Playground
import { Switch } from "@/components/shared/switch";
<Switch label="Online booking" defaultChecked />Sizes
md by default; sm for dense rows such as a table of services. Both reach past their edges for a comfortable hit area, and on a touch screen that hit area meets the 40px touch floor while the track keeps its size.
import { Switch } from "@/components/shared/switch";
<Switch defaultChecked aria-label="Online booking" />
<Switch size="sm" defaultChecked aria-label="Online booking" />States
Off is an ink tint, on is black; the white thumb casts a soft shadow, with no outline. Disabled dims the switch and ignores presses.
import { Switch } from "@/components/shared/switch";
<Switch aria-label="Off" />
<Switch defaultChecked aria-label="On" />
<Switch disabled aria-label="Disabled, off" />
<Switch disabled defaultChecked aria-label="Disabled, on" />With a label
label wraps the switch in a real <label>: clicking the text toggles it, and the text names it. Put the label after the switch inline, or before it with labelPosition="start".
import { Switch } from "@/components/shared/switch";
<Switch label="Send reminders the day before" defaultChecked />
<Switch label="Accept walk-ins" labelPosition="start" />
<Switch label="Deposits" disabled />Settings rows
Label first, switch at the far edge: stretch the label with className — with a label, it lays out the label, so the whole row is the target.
import { Switch } from "@/components/shared/switch";
{week.map((day, i) => (
<Switch
key={day}
label={day}
labelPosition="start"
className="flex min-h-11 w-full justify-between"
checked={open[i]}
onCheckedChange={(next) => setOpen(i, next)}
/>
))}Named from elsewhere
In a table row the name is already on screen: give the switch an aria-label that says which row it belongs to. Where a separate label sits apart from it, point the label's htmlFor at the switch's id.
import { Switch } from "@/components/shared/switch";
<TableCell>{service.name}</TableCell>
<TableCell>
<Switch
size="sm"
checked={service.active}
onCheckedChange={(active) => setActive(service, active)}
aria-label={`Active: ${service.name}`}
/>
</TableCell>import { Switch } from "@/components/shared/switch";
import { Label } from "@/components/shared/field";
<Label htmlFor="sunday">Open on Sundays</Label>
<Switch id="sunday" />API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| checked | boolean | — | Controlled state. Pair with onCheckedChange. |
| defaultChecked | boolean | false | Initial state when uncontrolled. |
| onCheckedChange | (checked: boolean, details) => void | — | Called with the new state on every toggle. |
| size | "sm" | "md" | "md" | 18px tall by default; 14px for dense rows. On a touch screen either one's hit area is 40px tall. |
| label | ReactNode | — | Visible text in a wrapping <label>: clicking it toggles, and it names the switch. |
| labelPosition | "end" | "start" | "end" | Label after the switch (inline) or before it (settings rows). Needs label. |
| disabled | boolean | false | Ignores presses and dims the switch; a label's text goes muted. |
| name | string | — | Submits with a form: "on" when checked, nothing when off (override with value / uncheckedValue). |
| id | string | — | Lands on the hidden input, so a <label htmlFor> elsewhere names and toggles the switch. |
| aria-label | string | — | The name when there's no visible label — say which row it belongs to. |
| className | string | — | Layout for the outermost element: the label when there is one, else the switch. |
Everything else passes through to the Base UI switch (readOnly, required, inputRef, …).
Guidelines
- A switch acts the moment it flips — online booking goes live, a service stops being bookable. If the change waits for a Save button, it is a form choice: use a RadioGroup instead.
- Label the setting, not the action: “Online booking”, not “Enable online booking”. The switch already says on or off — never add “On”/“Off” text beside it.
- Every switch has a name:
label, a label pointing at itsid, or anaria-labelthat names the row (“Active: Skin fade”), not just the column. - Use
mdeverywhere people tap; keepsmfor dense tables where the switch sits beside other data. - Disable a switch only when it truly can't change, and say why nearby — “Connect payments to take deposits”.