Switch for React
On and off for settings that apply right away, with role="switch", as a list or as rows.
One of 9 components in Forms and inputs
On and off, right away
A switch turns a setting on or off with immediate effect, like a light switch. This one is a real button with role switch and aria-checked, so screen readers announce it as on or off, Space toggles it, and the label is part of the click area.
Settings list stacks switches with a title and a sentence each, the layout of a notifications page. Rows with icons adds an icon per row and fits app settings such as Dark mode, Sounds and Auto save. Both show the state with position and color, and the thumb moves calmly, or not at all with reduced motion.
Use a switch only when the change applies at once. In a form that is saved with a button, use a checkbox instead. Label the setting, not the state, Email notifications rather than Turn on email, the switch itself shows whether it is on.
Details
What Switch brings along, read from the registry and the source.
- Category
- Forms and inputs
- Area
- Elements
- Variants
- 2
- Version
- 1.0.0 · since 1.0.0
- File
- components/ui/switch.tsx
- Brings along
- utilskiticon
- npm packages
- None
- Languages
- Texts come through props
- Studio
- Adjust in code
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The SwitchProps interface from components/ui/switch.tsx, with the comments from the code. Also takes every prop of Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'value'>.
checkedoptionalbooleandefaultCheckedoptionalbooleanonCheckedChangeoptional(checked: boolean) => voidlabeloptionalReact.ReactNodedescriptionoptionalReact.ReactNodesizeoptional'sm' | 'md'positionoptional'start' | 'end'Where the switch sits next to the label. end is the settings-list look.
variantoptional'default' | 'row'row: a bordered settings row, optionally with an icon
iconoptionalIconNamenameoptionalstringForm name, a hidden input carries "on" when checked
Accessibility
Read from the source of the component.
- Sets aria-checked, aria-describedby and aria-labelledby, so screen readers announce purpose and state.
- Uses the role switch.
- Every field has a label, visible or for screen readers.
- Shows a visible focus ring when used with the keyboard.
- The server renders the starting state in full, JavaScript only adds the interaction.
Motion
- With reduced motion (prefers-reduced-motion) it stays still. 2 places in the code switch motion off then.
- No animations, it stands still from the start.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 138 lines
'use client';
import { useId, useState } from 'react';
import { cn } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
export interface SwitchProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange' | 'value'> {
checked?: boolean;
defaultChecked?: boolean;
onCheckedChange?: (checked: boolean) => void;
label?: React.ReactNode;
description?: React.ReactNode;
size?: 'sm' | 'md';
/** Where the switch sits next to the label. end is the settings-list look. */
position?: 'start' | 'end';
/** row: a bordered settings row, optionally with an icon */
variant?: 'default' | 'row';
icon?: IconName;
/** Form name, a hidden input carries "on" when checked */
name?: string;
className?: string;
}
const SIZE = {
sm: { track: 'h-5 w-9', thumb: 'size-4', on: 'translate-x-4', mark: 8 },
md: { track: 'h-6 w-11', thumb: 'size-5', on: 'translate-x-5', mark: 10 },
};
/**
* An on/off setting that applies right away. A button with role="switch" and aria-checked.
* <Switch label="Email digest" description="A summary every Monday" defaultChecked />
*/
export function Switch({ checked, defaultChecked, onCheckedChange, label, description, size = 'md', position = 'end', variant = 'default', icon, name, className, id, disabled, onClick, ...rest }: SwitchProps) {
const auto = useId();
const sid = id ?? auto;
const [inner, setInner] = useState(defaultChecked ?? false);
const on = checked ?? inner;
const s = SIZE[size];
const control = (
<button
type="button"
role="switch"
id={sid}
aria-checked={on}
aria-labelledby={label ? `${sid}-label` : undefined}
aria-describedby={description ? `${sid}-desc` : undefined}
disabled={disabled}
{...rest}
onClick={(e) => {
onClick?.(e);
setInner(!on);
onCheckedChange?.(!on);
}}
className={cn(
'group/switch relative inline-flex shrink-0 cursor-pointer items-center rounded-full p-0.5 outline-none transition-[background-color,box-shadow] duration-200 ease-[var(--ease-kit)]',
'bg-[color-mix(in_oklab,var(--foreground)_14%,var(--background))] shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_6%,transparent)] hover:bg-[color-mix(in_oklab,var(--foreground)_20%,var(--background))]',
'aria-checked:bg-primary aria-checked:hover:bg-[color-mix(in_oklab,var(--primary)_88%,var(--background))]',
'focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_25%,transparent)]',
'disabled:cursor-not-allowed disabled:opacity-50',
// 44 px hit area
'after:absolute after:-inset-x-1 after:-inset-y-3 after:content-[""]',
s.track,
!label && className,
)}
>
{/* The knob travels on a spring: it overshoots a hair and settles. Held down it stretches. A small check draws itself on it. */}
<span
aria-hidden
className={cn(
'pointer-events-none grid place-items-center rounded-full shadow-[0_1px_3px_rgb(0_0_0/0.2),0_0_0_0.5px_rgb(0_0_0/0.04)] transition-[translate,scale,background-color] duration-[420ms] ease-[cubic-bezier(0.34,1.45,0.64,1)] group-active/switch:scale-x-[1.18] group-active/switch:duration-150 motion-reduce:transition-none',
s.thumb,
on ? cn(s.on, 'origin-right bg-primary-foreground') : 'origin-left bg-background dark:bg-[color-mix(in_oklab,var(--foreground)_88%,var(--background))]',
)}
>
<svg viewBox="0 0 24 24" width={s.mark} height={s.mark} fill="none" className="text-primary">
<path
d="m5 12.5 4.5 4.5L19 7.5"
pathLength={1}
strokeDasharray="1 1"
stroke="currentColor"
strokeWidth="3.4"
strokeLinecap="round"
strokeLinejoin="round"
className={cn('transition-[stroke-dashoffset,opacity] ease-[var(--ease-kit)] motion-reduce:transition-none', on ? 'opacity-100 delay-150 duration-300 [stroke-dashoffset:0]' : 'opacity-0 duration-150 [stroke-dashoffset:1]')}
/>
</svg>
</span>
</button>
);
const hidden = name ? <input type="hidden" name={name} value={on ? 'on' : ''} /> : null;
if (!label) return (
<>
{control}
{hidden}
</>
);
const text = (
<span className="min-w-0 flex-1">
<label id={`${sid}-label`} htmlFor={sid} className={cn('block cursor-pointer text-sm font-medium leading-5 text-foreground', disabled && 'cursor-not-allowed')}>
{label}
</label>
{description && (
<span id={`${sid}-desc`} className="mt-0.5 block text-[13px] leading-5 text-muted-foreground">
{description}
</span>
)}
</span>
);
return (
<div
className={cn(
'flex items-start gap-3.5',
variant === 'row' && 'items-center rounded-xl border border-border bg-card px-4 py-3.5 shadow-[var(--shadow-sm)]',
disabled && 'opacity-60',
className,
)}
>
{variant === 'row' && icon && (
<span
className={cn(
'grid size-9 shrink-0 place-items-center rounded-lg border bg-surface transition-[color,border-color,background-color] duration-300',
on ? 'border-[color-mix(in_oklab,var(--primary)_35%,var(--border))] bg-[color-mix(in_oklab,var(--primary)_8%,var(--surface))] text-[color-mix(in_oklab,var(--primary)_85%,var(--foreground))]' : 'border-border text-muted-foreground',
)}
>
<Icon name={icon} size={18} />
</span>
)}
{position === 'start' && <span className={cn('flex items-center', size === 'md' ? 'h-6 -mt-0.5' : 'h-5')}>{control}</span>}
{text}
{position === 'end' && <span className={cn('flex items-center', variant === 'row' ? '' : size === 'md' ? 'h-6 -mt-0.5' : 'h-5')}>{control}</span>}
{hidden}
</div>
);
}Frequent questions
How do I install Switch?
With aiw add @ai-website/switch. The command puts components/ui/switch.tsx into your project, the code is yours from then on. It brings along icon. It needs no further npm packages.
Which variants does Switch have?
2 variants, Settings list and Rows with icons.
Can an AI assistant build with Switch?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Switch with aiw_components and install it with aiw_add @ai-website/switch. Name the variant in your prompt, for example variant="row", and the assistant writes your content straight into the props.