Primitives
Switch
On/off control with token-driven track, thumb, and press motion.
Composition
On/off control
- <Switch />
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/switch.tsx.
npx shadcn@latest add https://control-ui.dev/r/switch.jsonDependencies
Use switch drag
src/registry/sources/control-ui/ui/use-switch-drag.tsSupportRaw code
Primary installed source
"use client";import { Switch as SwitchPrimitive } from "@base-ui/react/switch";import type { CSSProperties, ReactNode } from "react";import type { SwitchKnobStyle } from "@/components/control-ui/knob-contracts/switch-knobs";import { cn } from "@/components/control-ui/lib/cn";import { useSwitchDrag } from "./use-switch-drag";export type SwitchProps = Omit< { checked?: boolean; defaultChecked?: boolean; onCheckedChange?: (checked: boolean) => void; disabled?: boolean; readOnly?: boolean; required?: boolean; name?: string; value?: string; id?: string; className?: string; icon?: ReactNode; checkedIcon?: ReactNode; uncheckedIcon?: ReactNode; "aria-label"?: string; "aria-labelledby"?: string; "aria-describedby"?: string; "aria-invalid"?: boolean | "true" | "false"; }, "style"> & { style?: CSSProperties & SwitchKnobStyle };export function Switch({ className, checked, defaultChecked, onCheckedChange, disabled, readOnly, required, name, value, id, icon, checkedIcon, uncheckedIcon, style, ...props}: SwitchProps) { const { thumbRef, dragProps, dragProgress } = useSwitchDrag(); const dragStyle = { "--_switch-drag-progress": dragProgress ?? 0 }; const hasStateIcons = checkedIcon !== undefined || uncheckedIcon !== undefined; const hasIcon = icon !== undefined || hasStateIcons; const singleIcon = hasStateIcons ? undefined : icon; const onIcon = checkedIcon ?? icon; const offIcon = uncheckedIcon ?? icon; return ( <SwitchPrimitive.Root data-control-ui="switch" data-control-family="switch" data-slot="root" data-dragging={dragProgress !== null ? "" : undefined} data-drag-checked={dragProgress === null ? undefined : dragProgress >= 0.5} style={{ ...style, ...dragStyle }} checked={checked} defaultChecked={defaultChecked} onCheckedChange={onCheckedChange} disabled={disabled} readOnly={readOnly} required={required} name={name} value={value} id={id} nativeButton render={<button type="button" />} className={cn("relative inline-flex shrink-0 cursor-pointer items-center", "data-[disabled]:cursor-not-allowed", className)} {...props} {...dragProps} > <SwitchPrimitive.Thumb ref={thumbRef} data-control-ui="switch" data-control-family="switch" data-slot="thumb" className="pointer-events-none relative block" > {hasIcon ? <SwitchThumbIcon icon={singleIcon} checkedIcon={onIcon} uncheckedIcon={offIcon} /> : null} </SwitchPrimitive.Thumb> </SwitchPrimitive.Root> );}function SwitchThumbIcon({ icon, checkedIcon, uncheckedIcon }: { icon?: ReactNode; checkedIcon?: ReactNode; uncheckedIcon?: ReactNode }) { const base = "absolute inset-0 flex items-center justify-center [&_svg]:size-2.5 [&_svg]:stroke-[2.5]"; if (icon !== undefined) { return ( <span aria-hidden data-control-ui="switch" data-control-family="switch" data-slot="thumb-icon" className={base}> {icon} </span> ); } return ( <> {uncheckedIcon !== undefined ? ( <span aria-hidden data-control-ui="switch" data-control-family="switch" data-slot="thumb-icon" data-switch-icon="unchecked" className={base} > {uncheckedIcon} </span> ) : null} {checkedIcon !== undefined ? ( <span aria-hidden data-control-ui="switch" data-control-family="switch" data-slot="thumb-icon" data-switch-icon="checked" className={base} > {checkedIcon} </span> ) : null} </> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-switch-* · 19 knobsHow the cascade resolves--cui-switch-thumb-shadow*var(--shadow-sm)--cui-switch-radius<length-percentage>9999px--cui-switch-shadow*0 0 transparent--cui-switch-background<color>oklch(from var(--foreground) l c h / 0.14)--cui-switch-border-color<color>var(--control-boundary)--cui-switch-border-width<length>var(--control-rim-width)--cui-switch-checked-border-color<color>transparent--cui-switch-hover-background<color>oklch(from var(--foreground) l c h / 0.2)--cui-switch-checked-background<color>var(--primary)--cui-switch-checked-hover-background<color>color-mix(in oklch, var(--primary) 90%, var(--foreground))--cui-switch-thumb-radius<length-percentage>9999px--cui-switch-thumb-background<color>var(--background)--cui-switch-inline-size<length-percentage>calc(var(--spacing) * 9)--cui-switch-height<length-percentage>calc(var(--spacing) * 5)--cui-switch-padding<length-percentage>calc(var(--spacing) * 0.5)--cui-switch-thumb-inline-size<length-percentage>calc(var(--spacing) * 4)--cui-switch-thumb-press-inline-size<length-percentage>calc(var(--cui-switch-thumb-inline-size) * 1.25)--cui-switch-thumb-press-scale<number>1--cui-switch-thumb-backdrop-filter*none