zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
import { cx, useState } from "zeb/react";

/**
 * Switch — an on/off control. Radix's `Switch.Root` renders as a real
 * `<button>`, which is exactly what a plain `<button role="switch"
 * aria-checked>` gives natively — no hook needed beyond tracking the state.
 * Controlled via `checked`/`onCheckedChange` (upstream names), uncontrolled
 * via `defaultChecked`. `size` keeps upstream's `"sm" | "default"`. A
 * caller's own `onClick` is composed after the toggle handler (not
 * overwritten by it) — props are spread before the toggle's own `onClick` so
 * the composed handler always wins the DOM binding while everything else a
 * caller passes (data attributes, aria overrides, …) still comes through.
 */

const TRACK_SIZE = {
  default: "h-[1.15rem] w-8",
  sm: "h-3.5 w-6",
};

const THUMB_SIZE = {
  default: "size-4",
  sm: "size-3",
};

export function Switch({ className, checked, defaultChecked, onCheckedChange, disabled, size = "default", id, onClick, ...props }) {
  const [internal, setInternal] = useState(Boolean(defaultChecked));
  const isControlled = checked !== undefined;
  const isChecked = isControlled ? Boolean(checked) : internal;

  function handleClick(e) {
    if (disabled) return;
    const next = !isChecked;
    if (!isControlled) setInternal(next);
    onCheckedChange?.(next);
    onClick?.(e);
  }

  return (
    <button
      type="button"
      role="switch"
      id={id}
      aria-checked={isChecked}
      data-slot="switch"
      data-state={isChecked ? "checked" : "unchecked"}
      data-size={size}
      disabled={disabled}
      className={cx(
        "inline-flex shrink-0 items-center rounded-full border border-transparent shadow-xs outline-none transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20",
        TRACK_SIZE[size] ?? TRACK_SIZE.default,
        isChecked ? "bg-primary" : "bg-input",
        className
      )}
      {...props}
      onClick={handleClick}
    >
      <span
        data-slot="switch-thumb"
        data-state={isChecked ? "checked" : "unchecked"}
        className={cx(
          "pointer-events-none block rounded-full bg-background shadow-sm transition-transform",
          THUMB_SIZE[size] ?? THUMB_SIZE.default,
          isChecked ? "translate-x-[calc(100%-2px)]" : "translate-x-0"
        )}
      />
    </button>
  );
}

export default Switch;