stream/ui
Browse the library

Checkbox

A native checkbox with a hard-edged face. Fully compatible with forms.

stableCheckbox.tsx

Examples

Basic

checkbox/basic.tsx
import { Checkbox } from "@streamui/react";

export default function CheckboxBasic() {
  return <Checkbox label="Remember this device" defaultChecked />;
}

Group

Wrap related options in a fieldset with a legend.

Notify me about
checkbox/group.tsx
import { Checkbox } from "@streamui/react";

export default function CheckboxGroup() {
  return (
    <fieldset className="flex flex-col gap-4">
      <legend className="mb-4 font-bold">Notify me about</legend>
      <Checkbox name="notify" value="releases" label="New releases" defaultChecked />
      <Checkbox name="notify" value="security" label="Security advisories" defaultChecked />
      <Checkbox name="notify" value="digest" label="Weekly digest" />
      <Checkbox name="notify" value="billing" label="Billing (required)" checked disabled readOnly />
    </fieldset>
  );
}

Installation

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

<Checkbox name="terms" label="I agree to the terms" />

API

PropTypeDefaultDescription
labelReactNode—Visible label. The whole row is clickable.
checked / defaultCheckedboolean—Controlled or uncontrolled state, like a native input.
onChangeChangeEventHandler<HTMLInputElement>—Native change handler.
classNamestring—Classes for the outer <label>.
...propsOmit<ComponentProps<"input">, "type">—Native input attributes such as name and value.

Accessibility

  • The real <input> stays in the DOM, visually hidden, so keyboard and assistive tech work natively.
  • The visible box mirrors the input's focus ring through peer-focus-visible.

Source

packages/react/src/Checkbox.tsx
import type { ComponentProps, ReactNode } from "react";
import { Check } from "lucide-react";
import { cn } from "./cn";

export type CheckboxProps = Omit<ComponentProps<"input">, "type"> & {
  label?: ReactNode;
};

/**
 * A native checkbox with a brutal face. The real `<input>` stays in the DOM
 * (visually hidden), so forms, keyboard, and screen readers work unchanged.
 * Works controlled (`checked` + `onChange`) or uncontrolled (`defaultChecked`).
 */
export function Checkbox({ label, className, ...props }: CheckboxProps) {
  return (
    <label
      className={cn(
        "inline-flex cursor-pointer items-center gap-3 has-disabled:cursor-not-allowed has-disabled:opacity-50",
        className,
      )}
    >
      <input type="checkbox" className="peer sr-only" {...props} />
      <span
        aria-hidden
        className={cn(
          "grid size-6 shrink-0 place-items-center border-2 border-ink bg-paper-raised text-on-bright shadow-brutal-sm transition-colors duration-150",
          "peer-checked:bg-lime peer-focus-visible:outline-3 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-blue",
          "[&>svg]:opacity-0 peer-checked:[&>svg]:opacity-100",
        )}
      >
        <Check className="size-4" strokeWidth={3.5} />
      </span>
      {label && <span className="font-semibold">{label}</span>}
    </label>
  );
}