Switch
A binary on/off control - the checked track takes 4 semantic tones and the thumb animates on motion tokens.
import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
export default function SwitchDemo() {
return (
<Label>
<Switch tone="success" defaultChecked /> Auto-deploy on push
</Label>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/switchDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/switch.tsx"use client"
import type * as React from "react"
import { Switch as SwitchPrimitive } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const switchVariants = cva(
"peer relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-xs outline-none transition-colors duration-(--motion-fast) after:absolute after:-inset-1.5 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40 data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/70",
{
variants: {
tone: {
neutral: "data-[state=checked]:bg-primary",
brand: "data-[state=checked]:bg-brand",
success: "data-[state=checked]:bg-success",
danger: "data-[state=checked]:bg-danger",
},
size: {
sm: "h-4 w-7",
md: "h-5 w-9",
lg: "h-6 w-11",
},
},
defaultVariants: { tone: "neutral", size: "md" },
}
)
const thumbVariants = cva(
"pointer-events-none block rounded-full bg-background ring-0 transition-transform duration-(--motion-fast) data-[state=unchecked]:translate-x-0",
{
variants: {
size: {
sm: "size-3 data-[state=checked]:translate-x-3",
md: "size-4 data-[state=checked]:translate-x-4",
lg: "size-5 data-[state=checked]:translate-x-5",
},
},
defaultVariants: { size: "md" },
}
)
interface SwitchProps
extends React.ComponentProps<typeof SwitchPrimitive.Root>,
VariantProps<typeof switchVariants> {}
function Switch({ className, tone, size, ...props }: SwitchProps) {
return (
<SwitchPrimitive.Root
data-slot="switch"
data-tone={tone ?? "neutral"}
className={cn(switchVariants({ tone, size, className }))}
{...props}
>
<SwitchPrimitive.Thumb data-slot="switch-thumb" className={cn(thumbVariants({ size }))} />
</SwitchPrimitive.Root>
)
}
export { Switch, switchVariants, type SwitchProps }Manual installs skip the @ai2/tokens theme - add the token CSS from the theming guide or the tone colors will be missing.
Usage
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
<Label>
<Switch tone="success" defaultChecked /> Auto-deploy on push
</Label>Wrapping the switch in a Label makes the text clickable too. Both axes are optional - the default is the neutral medium switch.
Examples
Tones
The tone only shows while checked - every unchecked track uses the same neutral input color.
Sizes
Disabled
Invalid
Set aria-invalid when validation fails. The danger border and ring style is built in and matches every other ai2 form control.
Props
Switch also accepts every radix Switch prop - checked, defaultChecked, onCheckedChange and disabled included.
| Prop | Type | Default | Description |
|---|---|---|---|
tone | "neutral" | "brand" | "success" | "danger" | "neutral" | Semantic color of the checked track, mapped to theme tokens. Unchecked tracks always use the input token. |
size | "sm" | "md" | "lg" | "md" | Track and thumb dimensions, including thumb travel distance. |
ai2 Switch: a toggle for React with semantic tones and sizes
The ai2 Switch is a shadcn-compatible switch component for React, built on the radix-ui Switch primitive and styled with Tailwind CSS v4. It is the binary on and off control for settings screens, feature flags and preference panels, with a 4-tone by 3-size matrix from a single cva: the checked track takes a semantic color while every unchecked track stays on the neutral input token.
It ships through the shadcn registry format, so one CLI command, an MCP agent, or a copy-paste puts the source in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Switch?
It is a two-part render behind one export: Switch wraps the radix Switch.Root track and its Switch.Thumb, so usage stays a single self-closing tag. The anatomy matches shadcn/ui exactly, extended with two cva axes: tone (neutral, brand, success, danger) and size (sm, md, lg).
The tone only colors the checked state, mapping to the primary, brand, success and danger theme tokens. The size axis scales the track, the thumb and the thumb's travel distance together, and the travel animates on the shared --motion-fast token, so a small switch and a large switch feel like the same component.
Why use it
- Accessible by construction: The radix-ui Switch primitive renders a real button with role="switch" and aria-checked, toggles with Space and Enter, and participates in forms and labels like a native control.
- Semantic tones: neutral for ordinary settings, brand for promoted features, success for healthy states like auto-deploy, danger for risky toggles. The unchecked track always stays neutral, so meaning attaches to the on state only.
- Three coherent sizes: sm, md and lg scale the track, thumb and travel distance together from one cva pair, so density changes never break the geometry.
- Motion from tokens: Both the track color and the thumb travel transition on --motion-fast with the shared easing, matching every other ai2 component.
- Agent-readable metadata: The registry item states the tone and size matrix in plain words, so an MCP agent can find, inspect and install it without guessing.
Features
- shadcn registry install: One command adds the component, its radix-ui dependency and the @ai2/tokens theme to your project.
- 4 tones x 3 sizes: Twelve combinations from two cva axes, all resolved against theme tokens rather than hardcoded colors.
- Controlled and uncontrolled state: Use defaultChecked for simple cases or drive checked and onCheckedChange from React state when other UI depends on the value.
- Form-ready: The radix primitive supports name, value, required and disabled, and submits like a native checkbox inside a form.
- Data attributes for styling: The track exposes data-slot="switch", data-tone and data-state, the thumb exposes data-slot="switch-thumb", so you can restyle states from CSS without forking the component.
- Focus ring and invalid styling: Keyboard focus gets a 3px ring on the ring token, and aria-invalid switches the track to the danger border and ring, consistent with every ai2 form control.
Production tips
- Wrap it in a Label: Putting the Switch inside an ai2 Label makes the text clickable and gives screen readers the accessible name in one move. Standalone switches need an aria-label instead.
- Use switches for instant effects: A switch implies the change applies immediately. If the value only applies after a Save button, a checkbox communicates the deferred behavior better.
- Reserve danger for real risk: A red checked track reads as a warning. Use tone="danger" for toggles like public access or force deploys, not for ordinary off-by-default settings.
- Keep tone consistent per screen: Pick one tone for a settings group instead of mixing colors row by row; the tone should encode meaning, not decoration.
- Label the setting, not the action: Prefer "Auto-deploy on push" over "Enable auto-deploy". The switch itself already communicates on and off, so the label should name the thing being controlled.
Works with the rest of ai2
The switch composes naturally with the rest of the registry. Pair it with an ai2 Label for clickable text, lay out settings rows with ai2 Field, and group related toggles inside an ai2 Card with ai2 Separator rows between sections.
When the choice is a deferred selection rather than an instant toggle, use Checkbox instead, and for one-of-many options reach for Radio Group. All three share the same tone vocabulary and token source, so mixed forms stay visually consistent in both modes.