Switch
A toggle control that allows users to switch between on and off states.
The Switch component is a toggle control built on React Aria's Switch, providing accessible keyboard navigation, focus management, and smooth animations.
Basic Usage
A simple switch with a text label.
import { Switch } from "@repo/ui/components/switch";
/* BASIC USAGE EXAMPLE */
export const Example1 = () => {
return (
<div className="flex items-center gap-2">
<Switch id="airplane-mode" />
<label htmlFor="airplane-mode" className="text-sm">
Airplane Mode
</label>
</div>
);
};
Controlled Switch
Use isSelected and onChange props to control the switch state programmatically.
Notifications are disabled
"use client";
import { useState } from "react";
import { Switch } from "@repo/ui/components/switch";
/* CONTROLLED SWITCH EXAMPLE */
export const Example2 = () => {
const [isEnabled, setIsEnabled] = useState(false);
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<Switch
id="notifications"
isSelected={isEnabled}
onChange={setIsEnabled}
/>
<label htmlFor="notifications" className="text-sm">
Enable notifications
</label>
</div>
<p className="text-muted-foreground text-sm">
Notifications are{" "}
<span className="text-foreground font-medium">
{isEnabled ? "enabled" : "disabled"}
</span>
</p>
</div>
);
};
Disabled State
Use isDisabled to disable the switch. The component supports both off and on states while disabled.
import { Switch } from "@repo/ui/components/switch";
/* DISABLED SWITCH EXAMPLE */
export const Example3 = () => {
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<Switch id="disabled-off" isDisabled />
<label
htmlFor="disabled-off"
className="text-muted-foreground text-sm"
>
Disabled (off)
</label>
</div>
<div className="flex items-center gap-2">
<Switch id="disabled-on" isDisabled isSelected />
<label
htmlFor="disabled-on"
className="text-muted-foreground text-sm"
>
Disabled (on)
</label>
</div>
</div>
);
};
Default Selected
Use defaultSelected to set the initial state of an uncontrolled switch.
import { Switch } from "@repo/ui/components/switch";
/* DEFAULT SELECTED EXAMPLE */
export const Example4 = () => {
return (
<div className="flex items-center gap-2">
<Switch id="dark-mode" defaultSelected />
<label htmlFor="dark-mode" className="text-sm">
Dark mode
</label>
</div>
);
};
Form Layout
Switches work great in settings forms with labels and descriptions.
Receive emails about new products and features.
Receive notifications for friend requests and mentions.
Get notified about security updates and login attempts.
"use client";
import { useState } from "react";
import { Switch } from "@repo/ui/components/switch";
import { Label } from "@repo/ui/components/label";
/* SWITCH IN FORM LAYOUT */
export const Example5 = () => {
const [settings, setSettings] = useState({
marketing: false,
social: true,
security: true,
});
const updateSetting = (key: keyof typeof settings, value: boolean) => {
setSettings((prev) => ({ ...prev, [key]: value }));
};
return (
<div className="w-full max-w-md space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="marketing">Marketing emails</Label>
<p className="text-muted-foreground text-sm">
Receive emails about new products and features.
</p>
</div>
<Switch
id="marketing"
isSelected={settings.marketing}
onChange={(val) => updateSetting("marketing", val)}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="social">Social notifications</Label>
<p className="text-muted-foreground text-sm">
Receive notifications for friend requests and mentions.
</p>
</div>
<Switch
id="social"
isSelected={settings.social}
onChange={(val) => updateSetting("social", val)}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="security">Security alerts</Label>
<p className="text-muted-foreground text-sm">
Get notified about security updates and login attempts.
</p>
</div>
<Switch
id="security"
isSelected={settings.security}
onChange={(val) => updateSetting("security", val)}
/>
</div>
</div>
);
};
Component Code
"use client";
import {
Switch as AriaSwitch,
SwitchProps as AriaSwitchProps,
SwitchRenderProps,
} from "react-aria-components";
import { tv, cn } from "tailwind-variants";
import { focusRing } from "@repo/ui/lib/utils";
export interface SwitchProps extends Omit<AriaSwitchProps, "children"> {}
const track = tv({
extend: focusRing,
base: "box-border flex h-5 w-9 shrink-0 cursor-default items-center rounded-full border border-transparent px-px font-sans shadow-inner transition duration-200 ease-in-out",
variants: {
isSelected: {
false: "group-pressed:bg-accent border-input bg-input",
true: "forced-colors:bg-[Highlight]! group-pressed:bg-primary/80 bg-primary",
},
isDisabled: {
true: "group-selected:bg-primary/50 forced-colors:group-selected:bg-[GrayText]! border-input bg-muted forced-colors:border-[GrayText]",
},
},
});
const handle = tv({
base: "shadow-xs h-4 w-4 transform rounded-full outline-1 -outline-offset-1 outline-transparent transition duration-200 ease-in-out",
variants: {
isSelected: {
false: "bg-foreground translate-x-0",
true: "bg-primary-foreground translate-x-full",
},
isDisabled: {
true: "forced-colors:outline-[GrayText]",
},
},
compoundVariants: [
{
isSelected: false,
isDisabled: true,
class: "bg-muted-foreground/50",
},
{
isSelected: true,
isDisabled: true,
class: "bg-primary-foreground/70",
},
],
});
export function Switch({ className, ...restProps }: SwitchProps) {
return (
<AriaSwitch
{...restProps}
className={cn(
className,
"group relative flex items-center gap-2 text-sm text-neutral-800 transition [-webkit-tap-highlight-color:transparent] disabled:text-neutral-300 dark:text-neutral-200 dark:disabled:text-neutral-600 forced-colors:disabled:text-[GrayText]",
)}
>
{(renderProps: SwitchRenderProps) => (
<>
<div className={track(renderProps)}>
<span className={handle(renderProps)} />
</div>
</>
)}
</AriaSwitch>
);
}