react ~5 KB 0 deps v0.3.0 ↗ GitHub ↗

@mshafiqyajid/react-switch

Headless toggle switch hook and styled component. Controlled and uncontrolled, sizes, tones, label positioning, loading spinner. Async onChange (Promise-driven pending state with revert on rejection).

Playground #

Enable notifications
Props
TSX
import { SwitchStyled } from "@mshafiqyajid/react-switch/styled";
import "@mshafiqyajid/react-switch/styles.css";

<SwitchStyled
  checked={checked}
  onChange={setChecked}
  label="Enable notifications"
/>

confirm guard demo — async, rejects ~30% of the time

The switch enters a pending (grey) state for ~900ms. If the guard rejects, it snaps back.

Guarded toggle

Install #

npm install @mshafiqyajid/react-switch

Quick start #

import { SwitchStyled } from "@mshafiqyajid/react-switch/styled";
import "@mshafiqyajid/react-switch/styles.css";

const [on, setOn] = useState(false);
<SwitchStyled checked={on} onChange={setOn} label="Enable notifications" />

Async toggle #

Return a Promise from onChange to drive the pending state automatically. The switch shows a spinner during the promise, blocks further clicks, and reverts the optimistic value on rejection.

<SwitchStyled
  defaultChecked
  label="Email notifications"
  onChange={async (next) => {
    await fetch("/api/prefs/email", {
      method: "POST",
      body: JSON.stringify({ enabled: next }),
    });
  }}
/>

Headless #

import { useSwitch } from "@mshafiqyajid/react-switch";

const { switchProps, isChecked, isPending, toggle } = useSwitch({
  defaultChecked: false,
  onChange: async (next) => {
    await fetch("/api/prefs", { method: "POST", body: String(next) });
  },
});

return (
  <button {...switchProps} className={isChecked ? "on" : "off"} disabled={isPending}>
    <span className="thumb" />
  </button>
);

API #

PropTypeDefaultDescription
checkedbooleanControlled checked state
defaultCheckedbooleanfalseUncontrolled initial state
onChange(v: boolean) => void | Promise<void>Called on toggle. Return a Promise to drive a pending state automatically; reverts on rejection.
size"sm" | "md" | "lg""md"Size
tone"neutral" | "primary" | "success" | "danger""primary"Color when on
labelReactNodeLabel text
labelPosition"left" | "right""right"Label side
loadingbooleanfalseForce spinner in thumb. Auto-set when onChange returns a Promise.
disabledbooleanfalseDisable the switch

The hook also returns isPending and the rendered switch lands aria-busy="true" + data-pending="true" while the promise is in flight.

Edit this page on GitHub