zebflow 0.11.14

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

/**
 * Checkbox — shadcn's checkbox, on Zebflow's engine. Radix renders a
 * button-like control with a hidden native input underneath for form
 * submission; here the real native `<input type="checkbox">` IS the
 * control — visually hidden with `sr-only` inside a `<label>` — and a
 * sibling `<span>` draws the box from the same state, so keyboard, forms
 * and autofill all work natively. Controlled via `checked`/`onCheckedChange`
 * (upstream names), uncontrolled via `defaultChecked`; both accept upstream's
 * `checked="indeterminate"`. Indeterminate is not a DOM attribute — it can
 * only be set imperatively on the input element — so it is applied in an
 * effect via a ref, and the visible box gets a dash glyph with
 * `aria-checked="mixed"` rather than being coerced to a plain checked state.
 * The engine's Tailwind subset compiles no rule for the bare `peer` marker
 * class, so the focus ring can't be a `peer-focus-visible:` variant on the
 * sibling box — it is tracked with `onFocus`/`onBlur` state instead.
 */

export function Checkbox({ className, checked, defaultChecked, onCheckedChange, disabled, id, "aria-invalid": ariaInvalid, ...props }) {
  const [internal, setInternal] = useState(defaultChecked ?? false);
  const [focused, setFocused] = useState(false);
  const isControlled = checked !== undefined;
  const current = isControlled ? checked : internal;
  const isIndeterminate = current === "indeterminate";
  const isChecked = current === true;
  const inputRef = useRef(null);

  useEffect(() => {
    if (inputRef.current) inputRef.current.indeterminate = isIndeterminate;
  }, [isIndeterminate]);

  function handleChange(e) {
    const next = e.target.checked;
    if (!isControlled) setInternal(next);
    onCheckedChange?.(next);
  }

  const state = isIndeterminate ? "indeterminate" : isChecked ? "checked" : "unchecked";

  return (
    <label className={cx("relative inline-flex size-4 shrink-0", disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer")}>
      <input
        ref={inputRef}
        type="checkbox"
        id={id}
        checked={isChecked}
        onChange={handleChange}
        onFocus={() => setFocused(true)}
        onBlur={() => setFocused(false)}
        disabled={disabled}
        aria-checked={isIndeterminate ? "mixed" : isChecked}
        aria-invalid={ariaInvalid}
        className="sr-only"
        {...props}
      />
      <span
        data-slot="checkbox"
        data-state={state}
        aria-hidden="true"
        className={cx(
          "flex aspect-square size-4 items-center justify-center rounded-sm border border-input shadow-xs transition-shadow",
          focused ? "border-ring ring-[3px] ring-ring/50" : "",
          ariaInvalid ? "border-destructive ring-destructive/20" : "",
          isChecked || isIndeterminate ? "border-primary bg-primary text-primary-foreground" : "bg-input/30",
          className
        )}
      >
        {isIndeterminate ? (
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" className="size-3.5">
            <path d="M5 12h14" />
          </svg>
        ) : isChecked ? (
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" className="size-3.5">
            <path d="M20 6 9 17l-5-5" />
          </svg>
        ) : null}
      </span>
    </label>
  );
}

export default Checkbox;