Switch
Immediate-effect on/off toggle built on Radix Switch, with `sm` and `default` size presets.
Overview
Switch is built on @radix-ui/react-switch. Use it when toggling the control changes state right away - notifications on / off, dark mode, feature flags. For state that takes effect only on form submission, use Checkbox instead.
Two sizes - sm (compact) and default - are exposed via the size prop. The thumb consumes the size via a group-data-[size] selector so it stays proportional in both presets.
Preview
'use client';import { Label, Switch } from '@gremorie/rx-forms';export function SwitchPreview() { return ( <div className="flex items-center gap-2"> <Switch id="sw-demo" defaultChecked /> <Label htmlFor="sw-demo">Stream tokens</Label> </div> );}Anatomy
Switch pill track with a sliding thumb; toggles state on clickInstallation
bash npx gremorie@latest add rx-switch bash pnpm dlx gremorie@latest add rx-switch bash yarn dlx gremorie@latest add rx-switch bash bunx --bun gremorie@latest add rx-switch Usage
import { Label, Switch } from "@gremorie/rx-forms";
export function Example() {
return (
<div className="flex items-center gap-2">
<Switch id="notifications" defaultChecked />
<Label htmlFor="notifications">Push notifications</Label>
</div>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
API
<Switch>
| Prop | Type | Default | Description |
|---|---|---|---|
checked | boolean | - | Controlled checked state. |
defaultChecked | boolean | - | Uncontrolled initial state. |
onCheckedChange | (checked: boolean) => void | - | Fires when the user toggles the switch. |
disabled | boolean | false | Disables interaction. |
required | boolean | false | Required for form submission. |
name | string | - | Form field name. |
value | string | "on" | Form field value when checked. |
size | "sm" | "default" | "default" | Footprint preset. sm is h-3.5 w-6; default is h-[1.15rem] w-8. |
Forwards to SwitchPrimitive.Root. Renders an internal SwitchPrimitive.Thumb that translates on data-state=checked.
Composition
- Always pair with a
<Label>viahtmlFormatching the switch'sid. - Use a wrapper with
flex items-center gap-2(orjustify-betweenfor settings rows where the label sits on one side and the switch on the other). - For form-bound boolean values that take effect on submit, prefer
Checkboxinstead.
Variations
Settings row
The canonical pattern for settings pages - label on the left, switch on the right, both vertically centered.
<div className="flex items-center justify-between rounded-md border p-4">
<div className="grid gap-0.5">
<Label htmlFor="dark-mode">Dark mode</Label>
<p className="text-sm text-muted-foreground">
Use a dark theme across the app.
</p>
</div>
<Switch id="dark-mode" defaultChecked />
</div>Sizes
Two presets: sm for dense surfaces (sidebars, mini-toolbars, table cells) and default for standard settings rows.
'use client';import { Label, Switch } from '@gremorie/rx-forms';export function SwitchSizesPreview() { return ( <div className="flex items-center gap-6"> <div className="flex items-center gap-2"> <Switch id="sw-sm" size="sm" defaultChecked /> <Label htmlFor="sw-sm">Small</Label> </div> <div className="flex items-center gap-2"> <Switch id="sw-default" size="default" defaultChecked /> <Label htmlFor="sw-default">Default</Label> </div> </div> );}Disabled
disabled blocks interaction and drops opacity to 50%. The checked state stays visible so users can tell what the locked value is.
'use client';import { Label, Switch } from '@gremorie/rx-forms';export function SwitchDisabledPreview() { return ( <div className="flex flex-col gap-3"> <div className="flex items-center gap-2"> <Switch id="sw-disabled-off" disabled /> <Label htmlFor="sw-disabled-off">Disabled, off</Label> </div> <div className="flex items-center gap-2"> <Switch id="sw-disabled-on" disabled defaultChecked /> <Label htmlFor="sw-disabled-on">Disabled, on</Label> </div> </div> );}Controlled with optimistic update
For settings that hit the network, flip the switch optimistically and roll back on error.
function NotificationToggle() {
const [enabled, setEnabled] = React.useState(false);
async function handleToggle(next: boolean) {
setEnabled(next);
try {
await api.updateNotifications(next);
} catch {
setEnabled(!next);
toast.error('Could not update notifications.');
}
}
return (
<div className="flex items-center gap-2">
<Switch
id="email-notifications"
checked={enabled}
onCheckedChange={handleToggle}
/>
<Label htmlFor="email-notifications">Email notifications</Label>
</div>
);
}Accessibility
- ARIA: Radix renders
role="switch"witharia-checkedreflecting the state. Screen readers announce the control as "switch" plus the current state. - Keyboard:
Spacetoggles the switch;Tab/Shift+Tabmove focus. - Label association: clicking the
<Label>toggles the switch via standardhtmlForsemantics. - Disabled:
disabledremoves the switch from the tab order, dims the surface, and propagates fade to the linked label viapeer-disabled. - Distinction from Checkbox: Switch is for immediate state changes ("now"); Checkbox is for state captured on form submit ("on submit"). This affects both semantics and user expectations.