# Switch

A shadcn-based Switch with smooth thumb motion, two sizes, and accessible form semantics.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
"use client";

import { Switch } from "@/components/ui/switch";

export function SwitchDemo() {
  return (
    <label className="flex items-center gap-3 text-sm">
      <Switch animated={true} aria-invalid={false} aria-label={"Enable notifications"} defaultChecked={false} disabled={false} readOnly={false} size={"default"} />
      <span>{"Enable notifications"}</span>
    </label>
  );
}
```

## Installation [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/switch.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install @base-ui/react@^1.8.0 cn motion@^12.38.0
```

## Usage [#usage]

```tsx
"use client";

import { Switch } from "@/components/ui/switch";

export function Demo() {
  return (
    <label className="flex items-center gap-3 text-sm">
      <Switch defaultChecked />
      Enable notifications
    </label>
  );
}
```

Use your configured UI alias if it differs. Requires React 19, Tailwind CSS 4 with shadcn theme tokens (`background`, `foreground`, `input`, `primary`, `primary-foreground`, `ring`, and `destructive`), Base UI 1.8+, Motion 12.38+, and `cn`. No website-specific CSS or provider is required for LTR. For manual installation, install these packages and copy the complete Source into your UI directory, preserving the upstream MIT notice.

## Examples [#examples]

### Basic [#basic]

Wrap the switch and text in a label so the whole row is clickable. `defaultChecked` initializes uncontrolled state. Changing it does not update a mounted switch; the playground remounts when this configuration changes. A switch represents an immediate on/off setting, not a submit action or mixed selection.

<ComponentPreview name="switch-demo" title="switch-demo.tsx">
  <SwitchDemo />
</ComponentPreview>

### Sizes and states [#sizes-and-states]

Use `size="sm"` for a compact control. Disabled switches cannot change and are omitted from submission. Read-only switches remain focusable and retain their submitted value. Use `aria-invalid` for error semantics and styling, with `aria-describedby` connecting the error text. This does not implement validation by itself.

<ComponentPreview name="switch-states" title="switch-states.tsx">
  <SwitchStates />
</ComponentPreview>

### Controlled state, submission and reset [#controlled-state-submission-and-reset]

Use `checked` with `onCheckedChange` to control state. The callback receives the next boolean and Base UI event details immediately, without waiting for animation; `details.cancel()` rejects the change. Set `name` and `value` for form submission. The checked value defaults to `"on"`; unchecked switches are omitted unless `uncheckedValue` is set. Base UI does not synchronize uncontrolled visual state with native form reset. Restore controlled state in `onReset` when reset is needed, as shown here.

<ComponentPreview name="switch-form" title="switch-form.tsx">
  <SwitchForm />
</ComponentPreview>

## Behavior & Accessibility [#behavior--accessibility]

* Tab focuses the switch; Space toggles it. The default root is a span with switch semantics and a hidden native checkbox input. Use an enclosing label or Base UI Field. For a sibling `htmlFor` label, use `render={<button />}` and `nativeButton` as explained in the [Base UI API](https://base-ui.com/react/components/switch).
* `ref` targets the visible root and `inputRef` targets the native input. Root props, event handlers, state-aware `className`/`style`, and composition are forwarded; children are reserved for the thumb. `required` enables native constraint validation; `form` associates an external input with a form.
* The thumb slides over 200 ms using an interruptible ease-out transform, without bounce, scaling, or delayed state updates. `animated={false}` and reduced-motion preferences make thumb changes immediate. Reduced motion also disables the CSS track-color transition; `animated={false}` alone does not.
* Sizes retain the shadcn foundation: default track 32 × 18.4 px with a 16 px thumb, small track 24 × 14 px with a 12 px thumb. The invisible hit area extends beyond the track; leave adequate space between controls. Labels provide a larger interaction target.
* Light and dark styles inherit shadcn tokens. Use `dir="rtl"` or Base UI `DirectionProvider` for RTL thumb motion; an ancestor HTML `dir` alone does not configure Base UI direction. Keep visible keyboard-focus treatment when customizing classes.

## Storybook Stories (Optional) [#storybook-stories-optional]

Install Switch first, then add its portable stories to your existing React Storybook:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/switch-stories.json
```

This adds only `switch.stories.tsx` beside Switch in your configured UI directory. It does not reinstall Switch, add packages, or modify `.storybook/`. Review overwrite prompts for existing stories. Supply `@storybook/react` matching your Storybook version and React; stories are typechecked and composed against Storybook React 10.6.1. No extra example dependencies are required.

Include `../components/ui/**/*.stories.@(js|jsx|mjs|ts|tsx)` in `.storybook/main.ts`, adjusted for your UI directory. Import your application's Tailwind/global stylesheet in `.storybook/preview.ts` and include the UI/stories directory in Tailwind source scanning. Stories inherit your theme and decorators. Apply the dark-theme selector inside the preview iframe. The `autodocs` tag needs your existing `@storybook/addon-docs` configuration; Canvas and Controls do not.

## Props [#props]

| Prop            | Type                                                                | Default                            | Description                                                                                                     |
| --------------- | ------------------------------------------------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| animated        | boolean                                                             | true                               | Slides the thumb over 200 ms. Reduced motion makes this immediate.                                              |
| aria-invalid    | boolean \| 'true' \| 'false'                                        | Not set                            | Invalid semantics and styling; connect error text using aria-describedby.                                       |
| checked         | boolean                                                             | Not set                            | Controlled checked state. Pair with onCheckedChange.                                                            |
| className       | string \| ((state: Switch.Root.State) => string)                    | Not set                            | Classes merged with defaults, optionally derived from Base UI state.                                            |
| defaultChecked  | boolean                                                             | false                              | Initial uncontrolled state; changing it does not update a mounted switch.                                       |
| dir             | "ltr" \| "rtl"                                                      | DirectionProvider (ltr by default) | Thumb direction. Use dir or Base UI DirectionProvider for RTL.                                                  |
| disabled        | boolean                                                             | false                              | Prevents interaction and excludes the input from submission.                                                    |
| inputRef        | Ref\<HTMLInputElement>                                              | Not set                            | Ref to the native input. Ordinary ref targets the visible root.                                                 |
| name            | string                                                              | Not set                            | Name of the native form input.                                                                                  |
| nativeButton    | boolean                                                             | false                              | Set true when render supplies a native button.                                                                  |
| onCheckedChange | (checked: boolean, details: Switch.Root.ChangeEventDetails) => void | Not set                            | Called immediately with the next boolean and Base UI event details. Call details.cancel() to reject the change. |
| readOnly        | boolean                                                             | false                              | Prevents changes while preserving focus and submitted value.                                                    |
| render          | ReactElement \| ComponentRenderFn                                   | Not set (span)                     | Base UI composition. For a sibling label use render=\{\<button />} and nativeButton.                            |
| required        | boolean                                                             | false                              | Requires an active switch for native form validation.                                                           |
| size            | "default" \| "sm"                                                   | "default"                          | Track and thumb size.                                                                                           |
| uncheckedValue  | string                                                              | Not set                            | Optional submitted value when off; otherwise omitted.                                                           |
| value           | string                                                              | "on" (form submission)             | Submitted value when checked.                                                                                   |

Switch forwards Base UI Root props, handlers, state-aware className/style and React 19 refs. Children are reserved for the thumb. The default root is a span with switch semantics and a native input; use an enclosing label or Base UI Field.

## Source [#source]

```tsx
"use client";

// Adapted from shadcn/ui's Base Nova Switch (MIT).
// https://ui.shadcn.com/docs/components/base/switch
/*
MIT License
Copyright (c) 2023 shadcn

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
import { useDirection } from "@base-ui/react/direction-provider";
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
import { cn } from "cn";
import { motion, useReducedMotion } from "motion/react";
import type { ComponentProps } from "react";

export type SwitchProps = Omit<
  ComponentProps<typeof SwitchPrimitive.Root>,
  "children"
> & {
  size?: "sm" | "default";
  /** Animate thumb changes. Reduced-motion preferences take precedence. */
  animated?: boolean;
};

export const Switch = ({
  className,
  size = "default",
  animated = true,
  dir,
  ...props
}: SwitchProps) => {
  const reducedMotion = useReducedMotion();
  const inheritedDirection = useDirection();
  const direction = dir ?? inheritedDirection;
  const checkedOffset =
    direction === "rtl" ? "calc(-100% + 2px)" : "calc(100% - 2px)";

  return (
    <SwitchPrimitive.Root
      {...props}
      dir={direction}
      data-slot="switch"
      data-size={size}
      className={(state) =>
        cn(
          "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent bg-input outline-none transition-colors duration-150 motion-reduce:transition-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-8 data-[size=sm]:h-3.5 data-[size=sm]:w-6 data-checked:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 dark:data-unchecked:bg-input/80",
          typeof className === "function" ? className(state) : className
        )
      }
    >
      <SwitchPrimitive.Thumb
        data-slot="switch-thumb"
        className="pointer-events-none block shrink-0 group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3"
        render={(thumbProps, state) => (
          <span {...thumbProps} aria-hidden="true">
            <motion.span
              className="block size-full rounded-full bg-background ring-0 dark:group-data-checked/switch:bg-primary-foreground dark:group-data-unchecked/switch:bg-foreground"
              initial={false}
              animate={{
                // Keep matching percentage/pixel terms so Motion can interpolate.
                x: state.checked ? checkedOffset : "calc(0% - 0px)",
              }}
              transition={{
                duration: animated && !reducedMotion ? 0.2 : 0,
                ease: [0.22, 1, 0.36, 1],
              }}
            />
          </span>
        )}
      />
    </SwitchPrimitive.Root>
  );
};

```

## Credits [#credits]

* [shadcn/ui](https://ui.shadcn.com/docs/components/base/switch): Adapted Base Nova Switch foundation. Vandor UI adds Motion thumb transitions, direction support, and reduced-motion handling.
