Switch
Toggle switch built on Base UI. Supports controlled and uncontrolled modes with optional label.
<Switch label="Dark mode" defaultChecked />
<Switch label="Notifications" /><Switch disabled defaultChecked />
<Switch disabled />
<Switch label="With label" disabled />- This is a pre-styled wrapper around Base UI Switch. Refer to their docs for the full API.
- When
labelis provided, the switch is wrapped in a<label>element automatically.
import { Switch } from "@/ui";
<Switch label="Enable notifications" />src/ui/Switch.tsx
"use client";
import { Switch as BaseSwitch } from "@base-ui/react/switch";
import { cn } from "@/utils";
interface SwitchProps {
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
disabled?: boolean;
label?: string;
name?: string;
className?: string;
"aria-label"?: string;
}
export function Switch({
checked,
defaultChecked,
onCheckedChange,
disabled,
label,
name,
className,
"aria-label": ariaLabel,
}: SwitchProps) {
const track = (
<BaseSwitch.Root
checked={checked}
defaultChecked={defaultChecked}
onCheckedChange={onCheckedChange}
disabled={disabled}
name={name}
aria-label={label ? undefined : (ariaLabel ?? "Toggle")}
className={cn(
"relative inline-flex h-6 w-11 cursor-pointer items-center rounded-full bg-root-200 transition-colors",
"data-checked:bg-primary",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-50",
!label && className,
)}
>
<BaseSwitch.Thumb className="size-5 translate-x-0.5 rounded-full bg-root shadow-sm transition-transform data-checked:translate-x-5.5" />
</BaseSwitch.Root>
);
if (!label) return track;
return (
<label
className={cn(
"inline-flex items-center gap-2",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
className,
)}
>
{track}
<span className="text-sm text-root-contrast">{label}</span>
</label>
);
}