# Switch

> A toggle that powers on like a neon tube.

Category: Forms. Docs: https://get-neonkit.vercel.app/components/switch

## Installation

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

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/switch`.

npm dependencies: none. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { Switch } from "@/components/ui/switch"

<Switch label="Airplane mode" />
```

## Examples

### Settings list

Turning a switch on strikes the track like a neon tube before it settles.

```tsx
"use client";

import * as React from "react";
import { BatteryLow, Plane, Wifi, type LucideIcon } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";

type Setting = {
  id: string;
  icon: LucideIcon;
  title: string;
  description: string;
  checked: boolean;
  onCheckedChange: (checked: boolean) => void;
};

export default function SwitchDemo() {
  const [airplane, setAirplane] = React.useState(false);
  const [wifi, setWifi] = React.useState(true);
  const [lowPower, setLowPower] = React.useState(false);

  const settings: Setting[] = [
    {
      id: "setting-airplane",
      icon: Plane,
      title: "Airplane mode",
      description: "Turns off cellular, Wi-Fi and Bluetooth.",
      checked: airplane,
      onCheckedChange: (on) => {
        setAirplane(on);
        if (on) setWifi(false);
      },
    },
    {
      id: "setting-wifi",
      icon: Wifi,
      title: "Wi-Fi",
      description: wifi ? "Connected to Studio 5G" : "Not connected",
      checked: wifi,
      onCheckedChange: setWifi,
    },
    {
      id: "setting-low-power",
      icon: BatteryLow,
      title: "Low power mode",
      description: "Pauses downloads and background refresh until you charge.",
      checked: lowPower,
      onCheckedChange: setLowPower,
    },
  ];

  return (
    <div className="w-full max-w-sm divide-y divide-white/[0.06] rounded-xl border border-white/10 bg-white/[0.03]">
      {settings.map(({ id, icon: Icon, title, description, checked, onCheckedChange }) => (
        <div key={id} className="flex items-center gap-3.5 px-4 py-3.5">
          <span
            className={cn(
              "flex size-8 shrink-0 items-center justify-center rounded-lg border border-white/10 bg-white/[0.04] transition-colors duration-150",
              checked ? "text-white/90" : "text-white/45",
            )}
          >
            <Icon className="size-4" />
          </span>
          <div className="min-w-0 flex-1">
            <label htmlFor={id} className="block text-sm font-medium text-white/90">
              {title}
            </label>
            <p id={`${id}-description`} className="text-[13px] leading-[18px] text-white/55">
              {description}
            </p>
          </div>
          <Switch
            id={id}
            checked={checked}
            onCheckedChange={onCheckedChange}
            aria-describedby={`${id}-description`}
          />
        </div>
      ))}
    </div>
  );
}
```

### Sizes

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

export default function SwitchSizes() {
  return (
    <div className="flex flex-col gap-4">
      <Switch size="sm" defaultChecked label="Small" />
      <Switch size="md" defaultChecked label="Medium" />
      <Switch size="lg" defaultChecked label="Large" />
    </div>
  );
}
```

### Colors

Pass a palette name or any hex value to color.

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

const colors = ["cyan", "blue", "violet", "magenta", "pink", "red", "orange", "amber", "lime", "green"] as const;

export default function SwitchColors() {
  return (
    <div className="grid w-full max-w-md gap-5">
      <div className="flex flex-wrap justify-center gap-x-6 gap-y-4">
        {colors.map((color) => (
          <Switch key={color} color={color} defaultChecked label={color} className="capitalize" />
        ))}
      </div>
      <div className="flex justify-center">
        <Switch color="#ff6a00" defaultChecked label="Any hex works · #ff6a00" />
      </div>
    </div>
  );
}
```

### Controlled

Pair checked with onCheckedChange to drive other UI from the state.

```tsx
"use client";

import * as React from "react";
import { Switch } from "@/components/ui/switch";

export default function SwitchControlled() {
  const [shared, setShared] = React.useState(false);

  return (
    <div className="w-full max-w-sm rounded-xl border border-white/10 bg-white/[0.03] p-4">
      <Switch checked={shared} onCheckedChange={setShared} label="Share with a public link" />
      <p className="mt-1.5 pl-12 text-[13px] leading-[18px] text-white/55">
        {shared
          ? "Anyone with the link can view this board."
          : "Only invited members can view this board."}
      </p>
      <div className="mt-4 flex items-center justify-between border-t border-white/[0.06] pt-3 font-mono text-xs">
        <span className="text-white/40">checked</span>
        <span className="text-white/90">{String(shared)}</span>
      </div>
    </div>
  );
}
```

## API reference

### Switch

Renders a button with role="switch" and accepts its attributes; the ref points at the button. With a label, className and style go to the wrapper around both.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checked` | `boolean` | — | Controlled on state. |
| `defaultChecked` | `boolean` | `false` | Initial state when uncontrolled. |
| `onCheckedChange` | `(checked: boolean) => void` | — | Called with the new state on every toggle. |
| `label` | `ReactNode` | — | Clickable label after the switch, linked with aria-labelledby. |
| `name` | `string` | — | Renders a hidden input so the switch submits with a form when on. |
| `value` | `string` | `"on"` | Value submitted under name. |
| `color` | `NeonColor` | `inherited` | Palette name or hex. Falls back to --neon-accent, then cyan. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Track of 28, 36 or 44px wide. |
| `disabled` | `boolean` | `false` | Blocks interaction and dims the switch. |

## Accessibility

- Space and Enter toggle the switch, and aria-checked reports its state.
- Without a label, pass an aria-label or point a label element at the switch id.
- The power-on flicker is skipped when the user prefers reduced motion, and never plays for switches that are already on when they mount.

## Source

`components/ui/switch.tsx`

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

// Thumb travel = track width - thumb - 2px border - 2px padding.
const sizes = {
  sm: { track: "h-4 w-7", thumb: "size-3", on: "translate-x-3", root: "gap-2.5", label: "text-[13px] leading-5" },
  md: { track: "h-5 w-9", thumb: "size-4", on: "translate-x-4", root: "gap-3", label: "text-sm leading-5" },
  lg: { track: "h-6 w-11", thumb: "size-5", on: "translate-x-5", root: "gap-3", label: "text-base leading-6" },
} as const;

// A struck tube: two quick drops in brightness, then it holds.
const keyframes = `@keyframes neonkit-switch-ignite{0%{opacity:0}12%{opacity:.85}22%{opacity:.15}40%{opacity:1}52%{opacity:.4}68%,100%{opacity:1}}`;

export interface SwitchProps
  extends Omit<
    React.ButtonHTMLAttributes<HTMLButtonElement>,
    "color" | "onChange" | "value" | "defaultChecked"
  > {
  /** Palette name or any hex color. Inherits `--neon-accent` when omitted. */
  color?: NeonColor;
  size?: keyof typeof sizes;
  checked?: boolean;
  defaultChecked?: boolean;
  onCheckedChange?: (checked: boolean) => void;
  /** Submits `value` under this name when on, like a native checkbox. */
  name?: string;
  value?: string;
  /** Clickable label rendered after the switch. */
  label?: React.ReactNode;
}

const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
  (
    {
      className,
      style,
      color,
      size = "md",
      checked: checkedProp,
      defaultChecked = false,
      onCheckedChange,
      onClick,
      disabled,
      name,
      value = "on",
      label,
      id: idProp,
      ...props
    },
    ref,
  ) => {
    const autoId = React.useId();
    const id = idProp ?? autoId;
    const labelId = `${id}-label`;

    const [uncontrolled, setUncontrolled] = React.useState(defaultChecked);
    const isControlled = checkedProp !== undefined;
    const checked = isControlled ? checkedProp : uncontrolled;

    // Count off-to-on changes. Each one remounts the glow layer so the flicker
    // replays; a switch that is already on at mount stays steady.
    const [ignitions, setIgnitions] = React.useState(0);
    const [previous, setPrevious] = React.useState(checked);
    if (previous !== checked) {
      setPrevious(checked);
      if (checked) setIgnitions((count) => count + 1);
    }

    const s = sizes[size];

    const control = (
      <button
        ref={ref}
        type="button"
        role="switch"
        id={id}
        aria-checked={checked}
        aria-labelledby={label ? labelId : undefined}
        data-state={checked ? "checked" : "unchecked"}
        disabled={disabled}
        onClick={(event) => {
          onClick?.(event);
          if (event.defaultPrevented) return;
          if (!isControlled) setUncontrolled(!checked);
          onCheckedChange?.(!checked);
        }}
        style={label ? undefined : neonVars(color, style)}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)]",
          "group/switch relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-white/10 bg-white/[0.06] p-px outline-none",
          "transition-[background-color,border-color,transform,scale] duration-150 ease-out active:scale-[0.97]",
          "focus-visible:ring-2 focus-visible:ring-[rgb(var(--neon)/0.6)] focus-visible:ring-offset-2 focus-visible:ring-offset-[#05060a]",
          "disabled:cursor-not-allowed",
          !disabled && "hover:border-white/20 hover:bg-white/[0.09]",
          !label && "disabled:opacity-50",
          s.track,
          !label && className,
        )}
        {...props}
      >
        <span
          key={ignitions}
          aria-hidden
          className={cn(
            "pointer-events-none absolute -inset-px rounded-full border border-[rgb(var(--neon)/0.75)] bg-[rgb(var(--neon)/0.2)]",
            "shadow-[0_0_14px_rgb(var(--neon)/0.45),inset_0_0_8px_rgb(var(--neon)/0.3)] transition-shadow duration-200 ease-out",
            !disabled &&
              "group-hover/switch:shadow-[0_0_20px_rgb(var(--neon)/0.6),inset_0_0_8px_rgb(var(--neon)/0.3)]",
            checked ? "opacity-100" : "opacity-0",
            checked &&
              ignitions > 0 &&
              "[animation:neonkit-switch-ignite_350ms_linear_both] motion-reduce:[animation:none]",
          )}
        />
        <span
          aria-hidden
          className={cn(
            "pointer-events-none relative block rounded-full",
            "transition-[transform,translate,background-color,box-shadow] duration-[180ms] ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none",
            checked
              ? cn(s.on, "bg-[rgb(var(--neon))] shadow-[0_0_10px_rgb(var(--neon)/0.9),inset_0_1px_0_rgb(255_255_255/0.5)]")
              : "translate-x-0 bg-white/60 shadow-[0_1px_2px_rgb(0_0_0/0.4)]",
            s.thumb,
          )}
        />
      </button>
    );

    const extras = (
      <>
        {ignitions > 0 && <style>{keyframes}</style>}
        {name && (
          <input
            type="checkbox"
            hidden
            name={name}
            value={value}
            checked={checked}
            disabled={disabled}
            readOnly
          />
        )}
      </>
    );

    if (!label) {
      return (
        <>
          {control}
          {extras}
        </>
      );
    }

    return (
      <span
        style={neonVars(color, style)}
        className={cn(
          "[--neon:var(--neon-accent,34_211_238)]",
          "inline-flex items-center",
          disabled && "opacity-50",
          s.root,
          className,
        )}
      >
        {control}
        <label
          id={labelId}
          htmlFor={id}
          className={cn(
            "cursor-pointer select-none text-white/90",
            disabled && "cursor-not-allowed",
            s.label,
          )}
        >
          {label}
        </label>
        {extras}
      </span>
    );
  },
);
Switch.displayName = "Switch";

export { Switch };
```
