stream/ui
Browse the library

Switch

An on/off toggle for settings that apply immediately. Works controlled or uncontrolled.

stableSwitch.tsx

Examples

Basic

Email alerts
SMS alerts
Managed by admin
switch/basic.tsx
import { Switch } from "@streamui/react";

export default function SwitchBasic() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-5">
      <div className="flex items-center justify-between gap-4">
        <span id="switch-email" className="font-semibold">
          Email alerts
        </span>
        <Switch aria-labelledby="switch-email" defaultChecked />
      </div>
      <div className="flex items-center justify-between gap-4">
        <span id="switch-sms" className="font-semibold">
          SMS alerts
        </span>
        <Switch aria-labelledby="switch-sms" />
      </div>
      <div className="flex items-center justify-between gap-4">
        <span id="switch-locked" className="font-semibold text-ink-muted">
          Managed by admin
        </span>
        <Switch aria-labelledby="switch-locked" defaultChecked disabled />
      </div>
    </div>
  );
}

Controlled

Pass checked and onCheckedChange to own the state.

Maintenance mode

state: off

switch/controlled.tsx
"use client";

import { useState } from "react";
import { Switch } from "@streamui/react";

export default function SwitchControlled() {
  const [enabled, setEnabled] = useState(false);

  return (
    <div className="flex items-center gap-4">
      <Switch
        aria-labelledby="switch-maintenance"
        checked={enabled}
        onCheckedChange={setEnabled}
      />
      <div>
        <p id="switch-maintenance" className="font-semibold">
          Maintenance mode
        </p>
        <p className="font-mono text-xs text-ink-muted">
          state: {enabled ? "on" : "off"}
        </p>
      </div>
    </div>
  );
}

Installation

bun add @streamui/react
usage.tsx
import { Switch } from "@streamui/react";

<span id="alerts">Email alerts</span>
<Switch aria-labelledby="alerts" defaultChecked />

API

PropTypeDefaultDescription
checkedboolean—Controlled state. Omit to let the switch manage itself.
defaultCheckedbooleanfalseInitial state when uncontrolled.
onCheckedChange(checked: boolean) => void—Called with the new state on every toggle.
disabledbooleanfalseBlocks interaction.
...propsComponentProps<"button">—Native button attributes, e.g. aria-labelledby.

Accessibility

  • Uses role="switch" with aria-checked, so screen readers announce "on" or "off".
  • Needs an accessible name: aria-labelledby pointing at visible text, or aria-label.
  • Space and Enter toggle it, like a native button.

Source

packages/react/src/Switch.tsx
"use client";

import { useState, type ComponentProps } from "react";
import { cn } from "./cn";

export type SwitchProps = Omit<ComponentProps<"button">, "onChange"> & {
  checked?: boolean;
  defaultChecked?: boolean;
  onCheckedChange?: (checked: boolean) => void;
};

export function Switch({
  checked,
  defaultChecked = false,
  onCheckedChange,
  className,
  ...props
}: SwitchProps) {
  const [internal, setInternal] = useState(defaultChecked);
  const isOn = checked ?? internal;

  return (
    <button
      type="button"
      role="switch"
      aria-checked={isOn}
      onClick={() => {
        setInternal(!isOn);
        onCheckedChange?.(!isOn);
      }}
      className={cn(
        "relative inline-flex h-8 w-14 shrink-0 cursor-pointer items-center border-2 border-ink shadow-brutal-sm transition-colors duration-150 motion-reduce:transition-none",
        "disabled:pointer-events-none disabled:opacity-50",
        isOn ? "bg-lime" : "bg-paper-sunken",
        className,
      )}
      {...props}
    >
      <span
        aria-hidden
        className={cn(
          "size-5 border-2 border-ink bg-paper-raised transition-[translate] duration-150 ease-out motion-reduce:transition-none",
          isOn ? "translate-x-7" : "translate-x-1",
        )}
      />
    </button>
  );
}