docs / core / switch
Switch
a labelled on/off control that takes effect immediately. flipping it changes something now, with no save button after.
intensity · recedesrsc · client
provenance
authored in usvalayer
core / primitivesintensity
recedes · safe anywhere, including dense surfacescomposition
settings rows and preference panels, one per line with a labeldescription carries the consequence when the label alone is vaguenot for choices a form submits later. that is Checkboxnever unlabelled. the thumb alone says nothing about what it togglesa11y
role="switch" named by its label · toggles with space · disabled sets aria-disabled · focus ring on the trackjest-axedependencies
@base-ui/react (Switch + Field) · class-variance-authoritylive demo · try it out
Enable push notifications for updates.
customize
labelnames the control
descriptionhelper text under the switch
sizesm for dense rows
defaultCheckedinitial on state
disableddims and blocks the pointer
usage
import { Switch } from "usva/primitives/switch";
<Switch
label="Notifications"
description="Enable push notifications for updates."
/>props
| prop | type | default | notes |
|---|---|---|---|
| checked | boolean | — | controlled checked state. pair with onCheckedChange. |
| defaultChecked | boolean | false | initial state when uncontrolled. |
| onCheckedChange | (checked: boolean) => void | — | fires with the next state on every toggle. |
| disabled | boolean | false | dims to 50% and blocks the pointer. disables the whole field. |
| label | ReactNode | — | the field label, wired to the control by id. |
| description | ReactNode | — | helper text under the control. |
| size | "sm" | "md" | "md" | sm for dense rows. both keep a full-height hit area. |
get it
npx shadcn add https://usva.build/r/switch.jsonsource · components/ui/switch.tsxexactly what this command copies
"use client";
import { Field } from "@base-ui/react/field";
import { Switch as Base } from "@base-ui/react/switch";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import { cn } from "@/lib/utils";
const trackVariants = cva(
"relative inline-flex shrink-0 items-center rounded-full border border-border bg-surface-2 outline-none transition-control duration-base ease-soft before:absolute before:content-[''] data-[unchecked]:hover:border-border-strong data-[checked]:border-accent data-[checked]:bg-accent data-[checked]:bg-gradient-accent data-[checked]:glow-ring focus-visible:border-transparent focus-visible:ring-focus aria-invalid:border-danger data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
{
variants: {
size: {
sm: "h-5 w-9 before:-inset-y-3 before:-inset-x-1",
md: "h-6 w-11 before:-inset-y-2.5 before:inset-x-0",
},
},
defaultVariants: { size: "md" },
},
);
const thumbVariants = cva(
"block rounded-full bg-ink shadow-raised transition-layout duration-base ease-spring motion-reduce:transition-none motion-reduce:transform-none data-[checked]:bg-on-accent",
{
variants: {
size: {
sm: "h-4 w-4 translate-x-0.5 data-[checked]:translate-x-4",
md: "h-5 w-5 translate-x-0.5 data-[checked]:translate-x-5",
},
},
defaultVariants: { size: "md" },
},
);
// Indent the description to line up under the label: track width + the row gap.
const descIndent: Record<NonNullable<SwitchProps["size"]>, string> = {
sm: "pl-12",
md: "pl-14",
};
export interface SwitchProps
extends Omit<
React.ComponentPropsWithoutRef<typeof Base.Root>,
"render" | "className"
>,
VariantProps<typeof trackVariants> {
className?: string;
label?: React.ReactNode;
description?: React.ReactNode;
}
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
({ className, size, label, description, id, disabled, ...props }, ref) => {
const generatedId = React.useId();
const switchId = id ?? generatedId;
return (
<Field.Root className="flex flex-col gap-1" disabled={disabled}>
<div className="flex items-center gap-3">
<Base.Root
ref={ref}
id={switchId}
disabled={disabled}
className={cn(trackVariants({ size }), className)}
{...props}
>
<Base.Thumb className={thumbVariants({ size })} />
</Base.Root>
{label ? (
<Field.Label
htmlFor={switchId}
className="text-sm text-ink select-none data-[disabled]:opacity-50"
>
{label}
</Field.Label>
) : null}
</div>
{description ? (
<Field.Description
className={cn("text-xs text-muted", descIndent[size ?? "md"])}
>
{description}
</Field.Description>
) : null}
</Field.Root>
);
},
);
Switch.displayName = "Switch";