Selection & input
Switch
M3 switch with growing handle and optional icons.
Switches turn a single setting on or off immediately, like Wi-Fi. Use a checkbox instead when the change only applies after saving a form.
Installation
npx @grenishrai/expressive add switchThis copies switch.tsx into your components folder, along with what it needs from Expressive (utils, motion, styles), and installs @base-ui/react.
Usage
import { Switch } from "@/components/expressive/switch";
<label className="flex items-center gap-4">
<Switch defaultChecked /> Wi-Fi
</label>
<Switch icons checked={dark} onCheckedChange={setDark} />Props
Switch
| Prop | Type | Default | Description |
|---|---|---|---|
| checked / defaultChecked | boolean | — | On state. |
| onCheckedChange | (checked: boolean) => void | — | Called when toggled. |
| icons | boolean | false | Show check and close icons in the handle. |
| disabled | boolean | — | Prevents interaction. |
| motion | "expressive" | "standard" | "fluid" | "none" | theme default | Motion scheme for this component's transitions. none turns animation off. |
Accessibility
- Exposes
role="switch"witharia-checked; Space toggles it. - Label it with a wrapping
<label>oraria-label.
Source
This is the file the CLI writes into your project, shown with the default import paths. It's yours to change.
"use client";
import * as React from "react";
import { Switch as BaseSwitch } from "@base-ui/react/switch";
import { useFluidLayout } from "@/lib/expressive/use-fluid";
import { cn } from "@/lib/expressive/utils";
import type { MotionProp } from "@/lib/expressive/use-motion";
export interface SwitchProps extends Omit<BaseSwitch.Root.Props, "className"> {
className?: string;
/** Show check / close icons inside the handle. */
icons?: boolean;
motion?: MotionProp;
}
/*
* M3 switch: 52×32 track. The handle grows from 16px (off) to 24px (on),
* and to 28px while pressed; position and size move on a spatial spring.
*/
export function Switch({ className, icons = false, motion, ...props }: SwitchProps) {
const thumb = React.useRef<HTMLElement>(null);
// Fluid: the handle jumps to its new spot and springs there from the old one,
// stretching like a droplet on the way.
useFluidLayout(thumb, { attributes: ["data-checked"], motion });
return (
<BaseSwitch.Root
data-slot="switch"
data-motion={motion}
className={cn(
"group focus-ring relative inline-flex h-8 w-13 shrink-0 cursor-pointer items-center rounded-shape-full",
"border-2 border-outline bg-surface-container-highest",
"transition-[background-color,border-color] spring-effects-default",
"data-checked:border-primary data-checked:bg-primary",
"data-disabled:cursor-default data-disabled:opacity-(--ex-disabled-content)",
className,
)}
{...props}
>
<BaseSwitch.Thumb
ref={thumb}
data-slot="switch-thumb"
className={cn(
// A 40px state-layer target centered on the handle.
"state-layer pointer-events-none absolute top-1/2 left-3.5 flex size-10 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-shape-full text-on-surface",
"transition-[left] spring-spatial-fast fluid:transition-none",
"group-data-checked:left-[calc(100%-0.875rem)] group-data-checked:text-primary",
"group-hover:before:opacity-(--ex-state-hover) group-focus-visible:before:opacity-(--ex-state-focus) group-active:before:opacity-(--ex-state-pressed)",
)}
>
<span
className={cn(
"flex size-4 items-center justify-center rounded-shape-full bg-outline text-surface-container-highest",
"transition-[width,height,background-color] spring-spatial-fast",
"group-data-checked:size-6 group-data-checked:bg-on-primary group-data-checked:text-on-primary-container",
"group-active:size-7 group-hover:group-data-unchecked:bg-on-surface-variant group-data-checked:group-hover:bg-primary-container",
icons && "size-6",
"[&_svg]:size-4",
)}
>
{icons && (
<>
<svg viewBox="0 0 24 24" aria-hidden="true" className="hidden group-data-checked:block" fill="currentColor">
<path d="M9.55 18 3.85 12.3l1.43-1.43 4.27 4.28 9.17-9.18 1.43 1.43Z" />
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true" className="block group-data-checked:hidden" fill="currentColor">
<path d="M6.4 19 5 17.6l5.6-5.6L5 6.4 6.4 5l5.6 5.6L17.6 5 19 6.4 13.4 12l5.6 5.6-1.4 1.4-5.6-5.6Z" />
</svg>
</>
)}
</span>
</BaseSwitch.Thumb>
</BaseSwitch.Root>
);
}