Browse docs
Browse docs
A binary state that takes effect immediately — feature flags, preference toggles, "do this, don't do this". Use a Switch when there's no Save button between the toggle and the effect.
import { Switch } from '@dashforge/tw';
<Switch label="Public access" />Standalone:
const [enabled, setEnabled] = useState(false);
<Switch
label="Public access"
checked={enabled}
onCheckedChange={setEnabled}
/>Inside a form:
import { DashForm } from '@dashforge/forms';
import { Switch, Button } from '@dashforge/tw';
<DashForm onSubmit={updateSettings}>
<Switch name="publicAccess" label="Public access" defaultChecked />
<Switch name="emailNotifs" label="Email notifications" />
<Button type="submit" color="primary">Save</Button>
</DashForm>Both are binary. The difference is intent:
| Use Switch when... | Use Checkbox when... |
|---|---|
| The action takes effect immediately | The action is staged for a Save button |
| The label describes a setting ("Email notifications") | The label is a declarative statement ("I agree to the terms") |
| There's no concept of "form" — it's a config row | The control is part of a form submission |
When in doubt, ship a Checkbox. Switches are louder visually and should be reserved for "this matters, you're flipping it now" states.
import { Switch, Stack } from '@dashforge/tw';
<Stack gap={2}>
<Switch name="dark-mode" label="Dark mode" />
<Switch name="notifications" label="Email notifications" defaultChecked />
</Stack><Switch
label="Public access"
helper="Anyone with the link can view this workspace."
defaultChecked
/>Helper text appears below the label in a muted color. Keep it under one sentence — Switches live in settings lists where vertical density matters.
{/* Uncontrolled */}
<Switch label="Auto-publish" defaultChecked />
{/* Controlled — useful when the toggle drives a side effect immediately */}
<Switch
label="Maintenance mode"
checked={maintenanceMode}
onCheckedChange={async (next) => {
setMaintenanceMode(next);
await api.setMaintenance(next);
}}
/>For switches that hit the network on every change, render a loading hint in helper while the request is in flight.
import { Switch, Stack } from '@dashforge/tw';
<Stack gap={2}>
<Switch name="state-off" label="Off" />
<Switch name="state-on" label="On" defaultChecked />
<Switch name="state-disabled-off" label="Disabled" disabled />
<Switch name="state-disabled-on" label="Disabled + on" disabled defaultChecked />
</Stack><Switch
label="Premium feature"
helper="Upgrade your plan to enable."
disabled
/>The canonical use case — a settings card with toggle rows:
Email notifications
Send me product updates once a week.Two-factor auth
Require a one-time code at sign-in.import { Switch, Stack, Box, Typography, Divider } from '@dashforge/tw';
<Box variant="outlined" rounded="md">
<Stack direction="row" align="center" justify="between" gap={3} sx="p-4">
<Stack gap={0}>
<Typography variant="body2">Email notifications</Typography>
<Typography variant="caption" color="muted">
Send me product updates once a week.
</Typography>
</Stack>
<Switch name="email-notif" defaultChecked />
</Stack>
<Divider />
<Stack direction="row" align="center" justify="between" gap={3} sx="p-4">
<Stack gap={0}>
<Typography variant="body2">Two-factor auth</Typography>
<Typography variant="caption" color="muted">
Require a one-time code at sign-in.
</Typography>
</Stack>
<Switch name="2fa" />
</Stack>
</Box><div className="rounded-xl border border-neutral-200 dark:border-neutral-700 p-6 space-y-3">
<h3 className="text-lg font-semibold">Workspace settings</h3>
<Switch name="publicAccess"
label="Public access"
helper="Anyone with the link can view."
defaultChecked
/>
<Switch name="emailNotifs"
label="Email notifications"
helper="Weekly activity digest."
/>
<Switch name="auditLog"
label="Audit log"
helper="Record every action by every user. May impact performance."
/>
</div><Switch
name="maintenanceMode"
label="Maintenance mode"
access={{ requires: 'workspace.admin', when: 'denied:disable' }}
/>Three when modes ('denied:hide' | 'denied:disable' | 'denied:readonly') — same semantics as Button, TextField, Checkbox.
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | — | Field name. Registers with <DashForm> if present. |
label | ReactNode | — | Inline label. |
helper | ReactNode | — | Helper text below the label. |
checked | boolean | — | Controlled checked state. |
defaultChecked | boolean | false | Initial state when uncontrolled. |
onCheckedChange | (next: boolean) => void | — | Radix-style callback. |
disabled | boolean | false | Disables the row and dims the visuals. |
access | AccessSpec | — | RBAC gating. |
slotProps | { root?, track?, thumb?, label?, helper? } | — | Per-slot overrides. |
className | string | — | Merged onto root via tailwind-merge. |
| ...rest | Radix Switch.Root props | — | All Radix props pass through. |
root (wrapping <label>) · track (the rail) · thumb (the moving circle) · label · helper
@radix-ui/react-switch) — role="switch", aria-checked, keyboard navigation (Space and Enter to toggle), focus management are all the primitive's responsibility.transform: translateX + a hardware-accelerated transition — no layout thrash at 60fps even on cheap Android devices.<input type="checkbox"> mirrors the Switch's state so native <form> submission works. Most apps use <DashForm> and never look at this — but it's there if you need it.onCheckedChange triggers an API call, render the loading hint in helper AND consider disabled={isPending} so the user doesn't double-tap during the request.