Vela UI Docsvenizia.ai

Switch

A control that toggles between checked and unchecked states.

npx shadcn@latest add @velaui/switch
npx shadcn@latest add @velaui/switch

Examples

  • Default
  • Checked
  • With label
  • Disabled
  • Invalid

View code

Switch · Default

Installation

pnpm dlx velaui add @velaui/switch
pnpm dlx velaui add @velaui/switch

Code

switch-default.tsx
import { Switch } from "@/components/ui/switch";

export default function Example() {
  return <Switch />;
}

View code

Switch · Checked

Installation

pnpm dlx velaui add @velaui/switch
pnpm dlx velaui add @velaui/switch

Code

switch-checked.tsx
import { Switch } from "@/components/ui/switch";

export default function Example() {
  return <Switch defaultChecked />;
}

View code

Switch · With label

Installation

pnpm dlx velaui add @velaui/switch
pnpm dlx velaui add @velaui/switch

Code

switch-with-label.tsx
import { Switch, SwitchField } from "@/components/ui/switch";

export default function Example() {
  return (
    <SwitchField>
      <Switch id="notifications" defaultChecked />
      <label htmlFor="notifications">Enable notifications</label>
    </SwitchField>
  );
}

View code

Switch · Disabled

Installation

pnpm dlx velaui add @velaui/switch
pnpm dlx velaui add @velaui/switch

Code

switch-disabled.tsx
import { Switch } from "@/components/ui/switch";

export default function Example() {
  return (
    <div className="flex items-center gap-4">
      <Switch disabled />
      <Switch disabled defaultChecked />
    </div>
  );
}

View code

Switch · Invalid

Installation

pnpm dlx velaui add @velaui/switch
pnpm dlx velaui add @velaui/switch

Code

switch-invalid.tsx
import { Switch, SwitchField } from "@/components/ui/switch";

export default function Example() {
  return (
    <SwitchField>
      <Switch id="terms" aria-invalid="true" />
      <label htmlFor="terms">Accept terms</label>
    </SwitchField>
  );
}

Sizes

Usage

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

export default function Example() {
  return <Switch />;
}

Props

Switch

PropTypeDefault
size"sm" | "md" | "lg""md"

Takes these and every prop of SwitchProps.

SwitchField

Takes every prop of SwitchFieldProps.

Dependencies

npm
class-variance-authority@0.7.1radix-ui@1.6.0
Installs with it
nothing else
npm
@base-ui/react@1.6.0class-variance-authority@0.7.1
Installs with it
nothing else

Source

components/ui/switch.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Switch as SwitchPrimitive } from "radix-ui";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const switchVariants = cva(
  // The 2px transparent border IS the design's 2px inset — focus-unchecked and
  // invalid both draw a real border there, so reserving it keeps the thumb from
  // shifting when either state turns on.
  "peer group/switch relative inline-flex shrink-0 items-center rounded-full border-2 border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-unchecked:bg-bg-surface-2 data-checked:bg-bg-foreground data-unchecked:focus-visible:ring-0 data-checked:focus-visible:ring-2 data-checked:focus-visible:ring-[#737373bf] data-unchecked:focus-visible:border-border-default data-unchecked:focus-visible:bg-bg-surface-1 data-unchecked:aria-invalid:border-negative-middle data-unchecked:aria-invalid:bg-negative data-checked:aria-invalid:border-negative-middle data-checked:aria-invalid:bg-negative aria-invalid:focus-visible:ring-negative-middle/25 disabled:cursor-not-allowed data-unchecked:disabled:bg-bg-disabled data-checked:disabled:bg-bg-disabled",
  {
    variants: {
      size: {
        sm: "h-4 w-7",
        md: "h-[18px] w-8",
        lg: "h-[22px] w-10",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type SwitchProps = React.ComponentProps<typeof SwitchPrimitive.Root> &
  VariantProps<typeof switchVariants>;

function Switch({ className, size = "md", ...props }: SwitchProps) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      data-size={size}
      className={cn(switchVariants({ size }), className)}
      {...props}
    >
      <SwitchPrimitive.Thumb
        data-slot="switch-thumb"
        className="pointer-events-none block rounded-full bg-bg-main ring-0 transition-transform group-data-[size=sm]/switch:size-3 group-data-[size=md]/switch:size-3.5 group-data-[size=lg]/switch:size-[18px] group-aria-invalid/switch:bg-negative-middle data-unchecked:translate-x-0 data-checked:translate-x-full group-disabled/switch:bg-bg-surface-2"
      />
    </SwitchPrimitive.Root>
  );
}

export type SwitchFieldProps = React.ComponentProps<"div">;

/** Switch paired with its label — the "Switch.Field" part of the design. */
function SwitchField({ className, ...props }: SwitchFieldProps) {
  return (
    <div
      data-slot="switch-field"
      className={cn(
        "inline-flex items-center gap-2 text-sm leading-[1.4] font-medium text-text-strong",
        className,
      )}
      {...props}
    />
  );
}

export { Switch, SwitchField, switchVariants };
components/ui/switch.tsx
/* eslint-disable react-refresh/only-export-components */
import * as React from "react";
import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const switchVariants = cva(
  // The 2px transparent border IS the design's 2px inset — focus-unchecked and
  // invalid both draw a real border there, so reserving it keeps the thumb from
  // shifting when either state turns on. Base UI renders a span (not a button),
  // so disabled styling keys off data-disabled instead of :disabled.
  "peer group/switch relative inline-flex shrink-0 items-center rounded-full border-2 border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-unchecked:bg-bg-surface-2 data-checked:bg-bg-foreground data-unchecked:focus-visible:ring-0 data-checked:focus-visible:ring-2 data-checked:focus-visible:ring-[#737373bf] data-unchecked:focus-visible:border-border-default data-unchecked:focus-visible:bg-bg-surface-1 data-unchecked:aria-invalid:border-negative-middle data-unchecked:aria-invalid:bg-negative data-checked:aria-invalid:border-negative-middle data-checked:aria-invalid:bg-negative aria-invalid:focus-visible:ring-negative-middle/25 data-disabled:cursor-not-allowed data-unchecked:data-disabled:bg-bg-disabled data-checked:data-disabled:bg-bg-disabled",
  {
    variants: {
      size: {
        sm: "h-4 w-7",
        md: "h-[18px] w-8",
        lg: "h-[22px] w-10",
      },
    },
    defaultVariants: {
      size: "md",
    },
  },
);

export type SwitchProps = SwitchPrimitive.Root.Props &
  VariantProps<typeof switchVariants>;

function Switch({ className, size = "md", ...props }: SwitchProps) {
  return (
    <SwitchPrimitive.Root
      data-slot="switch"
      data-size={size}
      className={cn(switchVariants({ size }), className)}
      {...props}
    >
      <SwitchPrimitive.Thumb
        data-slot="switch-thumb"
        className="pointer-events-none block rounded-full bg-bg-main ring-0 transition-transform group-data-[size=sm]/switch:size-3 group-data-[size=md]/switch:size-3.5 group-data-[size=lg]/switch:size-[18px] group-aria-invalid/switch:bg-negative-middle data-unchecked:translate-x-0 data-checked:translate-x-full group-data-disabled/switch:bg-bg-surface-2"
      />
    </SwitchPrimitive.Root>
  );
}

export type SwitchFieldProps = React.ComponentProps<"div">;

/** Switch paired with its label — the "Switch.Field" part of the design. */
function SwitchField({ className, ...props }: SwitchFieldProps) {
  return (
    <div
      data-slot="switch-field"
      className={cn(
        "inline-flex items-center gap-2 text-sm leading-[1.4] font-medium text-text-strong",
        className,
      )}
      {...props}
    />
  );
}

export { Switch, SwitchField, switchVariants };