Checkbox

A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time.
Installation

Install the following dependencies:

pnpm
npm
yarn
bun
yarn add @zag-js/checkbox @zag-js/react

This component is built directly on the Zag.js state machine (not a wrapper). No Portal or presence hook needed, the check mark just toggles opacity, no exit animation. The hidden native input (needed for keyboard, focus and form submission) renders automatically inside CheckboxRoot. Copy and paste the following code into your project.

components/ui/checkbox.tsx
Expand

import { createContext, useContext, useId } from "react";
import { twMerge } from "tailwind-merge";
import { useMachine, normalizeProps } from "@zag-js/react";
import * as checkbox from "@zag-js/checkbox";
import { Frame, parsePaths } from "@/components/ui/frame";
import { Check } from "lucide-react";

const CheckboxContext = createContext<ReturnType<typeof checkbox.connect> | null>(
  null
);

function useCheckboxContext() {
  const api = useContext(CheckboxContext);
  if (!api) throw new Error("Checkbox parts must be used within <CheckboxRoot>");
  return api;
}

function CheckboxRoot({
  children,
  className,
  ...rest
}: React.PropsWithChildren<Partial<checkbox.Props> & { className?: string }>) {
  const service = useMachine(checkbox.machine, { id: useId(), ...rest });
  const api = checkbox.connect(service, normalizeProps);

  return (
    <CheckboxContext.Provider value={api}>
      <label
        {...api.getRootProps()}
        className={twMerge(["flex gap-3.5 items-center cursor-pointer", className])}
      >
        {children}
        <input {...api.getHiddenInputProps()} />
      </label>
    </CheckboxContext.Provider>
  );
}

function CheckboxLabel({
  children,
  className,
}: React.PropsWithChildren<{ className?: string }>) {
  const api = useCheckboxContext();

  return (
    <span {...api.getLabelProps()} className={twMerge(["order-2", className])}>
      {children}
    </span>
  );
}

function CheckboxControl({ className }: { className?: string }) {
  const api = useCheckboxContext();

  return (
    <div
      {...api.getControlProps()}
      className={twMerge([
        "group relative size-5 flex items-center justify-center data-[state=checked]:drop-shadow-[0_0px_20px_var(--color-primary)]",
        "[--color-frame-1-stroke:var(--color-primary)]/80",
        "[--color-frame-1-fill:var(--color-primary)]/10",
        className,
      ])}
    >
      <Frame
        paths={parsePaths(
          '[{"show":true,"style":{"strokeWidth":"1","stroke":"var(--color-frame-1-stroke)","fill":"var(--color-frame-1-fill)"},"path":[["M","50% - 28.125%","0"],["L","50% + 28.125%","0"],["L","100% + 0","50% - 28.125%"],["L","100% + 0","50% + 28.125%"],["L","50% + 28.125%","100% - 0"],["L","50% - 28.125%","100% + 0"],["L","0","50% + 28.125%"],["L","0","50% - 28.125%"],["L","50% - 28.125%","0"]]}]'
        )}
      />
      <Check className="group-data-[state=checked]:opacity-100 opacity-0 size-6 -mt-1 -mr-2 stroke-(--color-primary)/80 drop-shadow-[0_0px_2px_var(--color-primary)] transition-all duration-100" />
    </div>
  );
}

export { CheckboxRoot, CheckboxLabel, CheckboxControl };
 
Expand

Update the import paths to match your project setup.

Usage
Expand

import {
  CheckboxRoot,
  CheckboxLabel,
  CheckboxControl,
} from "@/components/ui/checkbox";
 
Expand
Expand

<CheckboxRoot>
  <CheckboxLabel>Accept terms and conditions</CheckboxLabel>
  <CheckboxControl />
</CheckboxRoot>
 
Expand
Powered by synthetic caffeine · Deployed by Left4code · Signal traceable on GitHub.