For the complete documentation index, see 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.
1
Support

Switch

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

Playground

Live preview
Click the label or press Space to toggle. Code captures configuration, not transient clicks. Initially checked remounts the switch; Reset restores configuration and interaction state.
Customize Switch
Customize
switch-demo.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

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

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/switch.json

Dependencies

Installed automatically by the registry CLI. For manual setup:

Dependencies3 required packages
  • @base-ui/react@^1.8.0
  • cn
  • motion@^12.38.0
$ pnpm add @base-ui/react@^1.8.0 cn motion@^12.38.0

Usage

"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

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.

switch-demo.tsx
"use client";
/* eslint-disable jsx-a11y/label-has-associated-control -- Switch supplies the enclosed native input. */

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

export const SwitchDemo = () => (
  <label className="flex items-center gap-3 text-sm">
    <Switch defaultChecked />
    Enable notifications
  </label>
);

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.

Account alerts are required for this workspace.

switch-states.tsx
"use client";
/* eslint-disable jsx-a11y/label-has-associated-control -- Switch supplies the enclosed native input. */

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

export const SwitchStates = () => (
  <div className="flex flex-col gap-5">
    <label className="flex items-center gap-3 text-sm">
      <Switch size="sm" defaultChecked />
      Small switch
    </label>
    <label className="flex items-center gap-3 text-sm text-muted-foreground">
      <Switch disabled defaultChecked />
      Disabled
    </label>
    <label className="flex items-center gap-3 text-sm">
      <Switch readOnly defaultChecked />
      Read only
    </label>
    <div className="flex flex-col gap-2">
      <label className="flex items-center gap-3 text-sm">
        <Switch aria-invalid aria-describedby="switch-states-error" />
        Enable account alerts
      </label>
      <p id="switch-states-error" className="text-xs text-destructive">
        Account alerts are required for this workspace.
      </p>
    </div>
  </div>
);

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.

Submit to inspect the form value.
switch-form.tsx
"use client";
/* eslint-disable jsx-a11y/label-has-associated-control -- Switch supplies the enclosed native input. */

import { useState } from "react";

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

export const SwitchForm = () => {
  const [checked, setChecked] = useState(true);
  const [result, setResult] = useState("Submit to inspect the form value.");
  return (
    <form
      className="flex flex-col gap-4"
      onSubmit={(event) => {
        event.preventDefault();
        setResult(
          `updates: ${new FormData(event.currentTarget).get("updates")}`
        );
      }}
      onReset={() => {
        setChecked(true);
        setResult("Submit to inspect the form value.");
      }}
    >
      <label className="flex items-center gap-3 text-sm">
        <Switch
          name="updates"
          value="weekly"
          uncheckedValue="off"
          checked={checked}
          onCheckedChange={setChecked}
        />
        Weekly updates
      </label>
      <div className="flex gap-2">
        <Button type="submit" size="sm">
          Submit
        </Button>
        <Button type="reset" variant="outline" size="sm">
          Reset
        </Button>
      </div>
      <output className="text-xs text-muted-foreground" aria-live="polite">
        {result}
      </output>
    </form>
  );
};

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.
  • 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)

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

$ pnpm dlx 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

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
animatedboolean

Slides the thumb over 200 ms. Reduced motion makes this immediate.

true
aria-invalidboolean | 'true' | 'false'

Invalid semantics and styling; connect error text using aria-describedby.

Not set
checkedboolean

Controlled checked state. Pair with onCheckedChange.

Not set
classNamestring | ((state: Switch.Root.State) => string)

Classes merged with defaults, optionally derived from Base UI state.

Not set
defaultCheckedboolean

Initial uncontrolled state; changing it does not update a mounted switch.

false
dir"ltr" | "rtl"

Thumb direction. Use dir or Base UI DirectionProvider for RTL.

DirectionProvider (ltr by default)
disabledboolean

Prevents interaction and excludes the input from submission.

false
inputRefRef<HTMLInputElement>

Ref to the native input. Ordinary ref targets the visible root.

Not set
namestring

Name of the native form input.

Not set
nativeButtonboolean

Set true when render supplies a native button.

false
onCheckedChange(checked: boolean, details: Switch.Root.ChangeEventDetails) => void

Called immediately with the next boolean and Base UI event details. Call details.cancel() to reject the change.

Not set
readOnlyboolean

Prevents changes while preserving focus and submitted value.

false
renderReactElement | ComponentRenderFn

Base UI composition. For a sibling label use render={<button />} and nativeButton.

Not set (span)
requiredboolean

Requires an active switch for native form validation.

false
size"default" | "sm"

Track and thumb size.

"default"
uncheckedValuestring

Optional submitted value when off; otherwise omitted.

Not set
valuestring

Submitted value when checked.

"on" (form submission)

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

"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

  • shadcn/ui: Adapted Base Nova Switch foundation. Vandor UI adds Motion thumb transitions, direction support, and reduced-motion handling.