Skip to content
Beta · ExperimentalReact 19No peer dependencies

Checkbox

Checkbox and CheckboxGroup — the native checkbox, restyled, with labels, hints, errors, a mixed state and select all.

Beta components are subject to change and may break your code. Use them at your own risk, and share feedback through the bug tracker.

On this page

Installation

bash
npx gbs-add-block@latest -a Checkbox -beta

The block copies the checkbox folder into your project, along with the small shared folder that every component imports. You own the code and can change it freely. There are no peer dependencies other than React.

Requirements

  • React 19 and @types/react 19
  • TypeScript target ES2022 or newer, with "jsx": "react-jsx"
  • Browsers from 2024 or newer (the styles use light-dark(), color-mix() and :indeterminate)

Import the stylesheet once, for example in your global CSS:

css
@import "../components/checkbox/styles.css";

or in your root layout / entry file:

ts
import "@/components/checkbox/styles.css";

Checkbox is a single on/off choice with a label, hint and error, including a partly checked state. CheckboxGroup is a labelled set of checkboxes sharing one array of values, with an optional "select all" box. The box is the browser's own <input type="checkbox">, restyled. Forms post it, form libraries register it, and screen readers announce it exactly as a native checkbox, including "mixed".

Set the --gbs-* variables on :root to theme every component at once, the grid included. Each component's own variables fall back to them, and then to the built-in palette, so components look identical out of the box.

Checkbox

Live preview

CheckboxGroup

Live preview

Quick Start

tsx
"use client";
 
import { useState } from "react";
import { Checkbox, CheckboxGroup } from "@/components/checkbox";
 
export default function Preferences() {
  const [terms, setTerms] = useState(false);
  const [channels, setChannels] = useState<string[]>(["email"]);
 
  return (
    <>
      <Checkbox label="I accept the terms" checked={terms} onCheckedChange={setTerms} required />
 
      <CheckboxGroup
        label="Notify me by"
        name="channels"
        options={[
          { value: "email", label: "Email" },
          { value: "sms", label: "SMS", description: "Urgent alerts only" },
          { value: "push", label: "Push" },
        ]}
        value={channels}
        onValueChange={setChannels}
        selectAll
      />
    </>
  );
}

Both are controlled (checked / value) or uncontrolled (defaultChecked / defaultValue).

Props Table

Checkbox

Accepts every <input> attribute except type, plus:

PropTypeDefaultDescription
checkedboolean | "indeterminate"State (controlled).
defaultCheckedboolean | "indeterminate"falseStarting state (uncontrolled).
onCheckedChange(checked: boolean) => voidFires on toggle. Clicking an indeterminate box checks it.
valuestring"on"Posted when checked. Inside a group, the value this box adds.
labelReactNodeClickable label.
descriptionReactNodeHint under the label.
errorReactNodeError under the label; sets aria-invalid.
size"sm" | "md" | "lg""md"Box 14 / 16 / 20 px. Inside a group, defaults to the group's size.
classNamestringClass for the row.
classNamesPartial<Record<CheckboxSlot, string>>Slots: root, control, input, label, description, error.
refRef<HTMLInputElement>The <input>.

CheckboxGroup

PropTypeDefaultDescription
valuestring[]Checked values (controlled).
defaultValuestring[][]Starting values (uncontrolled).
onValueChange(values: string[]) => voidFires on every change.
options{ value, label, description?, disabled? }[]The boxes to render. Or pass <Checkbox value> children.
labelReactNodeThe group's name, as a <legend>.
descriptionReactNodeHint under the legend.
errorReactNodeError under the group; marks every box invalid.
requiredbooleanfalseMarks the legend.
disabledbooleanfalseDisables every box (through the native <fieldset disabled>).
namestringForm field name for every box, so the form posts each checked value.
size"sm" | "md" | "lg""md"Size of every box.
orientation"vertical" | "horizontal""vertical"Stack or wrap in a row.
selectAllboolean | ReactNodeWith options: a parent box that checks or clears them all. true uses localeText.selectAll.
id, className, classNames, styleSlots: root, legend, description, items, error.
localeTextPartial<CheckboxLocaleText>EnglishSee Locale Text.

Indeterminate State

tsx
<Checkbox label="Select all rows" checked={someSelected ? "indeterminate" : allSelected} onCheckedChange={toggleAll} />

"indeterminate" shows a dash and is announced as "mixed". It is a display state only: clicking the box makes it checked, and onCheckedChange receives true.

Groups

With options, which supports select-all:

tsx
<CheckboxGroup label="Permissions" options={permissions} value={granted} onValueChange={setGranted} selectAll />
  • Select all: shows checked, unchecked or mixed. Clicking it selects every enabled option, or clears them when all are already selected.
  • Disabled options: they keep their state either way.

With children, for custom layouts:

tsx
<CheckboxGroup label="Size" orientation="horizontal" defaultValue={["md"]}>
  <Checkbox value="sm" label="Small" />
  <Checkbox value="md" label="Medium" />
  <Checkbox value="lg" label="Large" />
</CheckboxGroup>

Any Checkbox with a value inside a group reads and updates the group's array, at any depth.

Forms

tsx
<form action={savePreferences}>
  <Checkbox name="newsletter" value="yes" label="Send me the newsletter" />
  <CheckboxGroup name="channels" options={channels} />
  <button type="submit">Save</button>
</form>

On the server, formData.get("newsletter") is "yes" when checked, and formData.getAll("channels") lists the checked values. Unchecked boxes post nothing, as native checkboxes do.

With react-hook-form, register a single checkbox like a native one:

tsx
<Checkbox label="Remember me" {...register("remember")} />

Keyboard

KeysAction
SpaceToggle the focused box.
Tab / Shift + TabMove between boxes.

Accessibility: every box is a native checkbox with a real <label>. A group is a <fieldset> with a <legend>, so screen readers announce the group name when entering it. Descriptions and errors are linked through aria-describedby, and errors are announced with role="alert". The mixed state comes from the native indeterminate property.

Styling and Theming

All rules are in the CSS components layer, so utility classes passed through className / classNames override them.

tsx
<Checkbox classNames={{ input: "rounded-full", label: "font-medium" }} />

CSS variables

Override them on .ck-root, .ck-group, :root, or through style. Each variable falls back to the shared --gbs-* of the same name, then to the DataGrid's --dg-* when that stylesheet is loaded, and finally to the built-in palette.

VariableUsed for
--ck-sizeBox size (set by size).
--ck-radiusBox corner radius.
--ck-borderUnchecked border.
--ck-input-bgUnchecked background.
--ck-accent, --ck-accent-fgChecked background and check mark.
--ck-fg, --ck-mutedLabel and hint text.
--ck-focusFocus ring.
--ck-dangerErrors.
--ck-font-sizeFont size.

Data attributes

ElementAttributes
Row (.ck-root)data-size, data-disabled, data-invalid
Box (.ck-input)the native :checked, :indeterminate and :disabled states
Group (fieldset.ck-group)data-orientation, data-select-all, data-invalid

Locale Text

tsx
<CheckboxGroup localeText={{ selectAll: "Alle auswählen" }} />
KeyDefault
selectAll"Select all"

Headless Use

ExportDescription
toggleValue(values, value, checked)Adds or removes one value.
groupState(values, options)true, false or "indeterminate" for a select-all box.
toggleAll(values, options)What select-all does, leaving disabled options alone.
CheckboxGroupContextBuild your own group layout that Checkboxes join.

Next.js

Both are client components with "use client" at the top of their files. They render the same markup on the server and the client. A controlled indeterminate state is applied right after hydration, because it exists only as a DOM property.

Migrating from the Previous Check Box

PreviousNew
CheckBoxCheckbox
checked, label, name, id, disabled, classNameSame names
indeterminate={true}checked="indeterminate"
onChange={(event) => setChecked(event.checked)}onCheckedChange={setChecked}, or native onChange={(event) => setChecked(event.target.checked)}
modeNot needed; use size, className or classNames
New: description, error, size, CheckboxGroup with select-all, ref to the input

Notes

  • A checkbox inside a CheckboxGroup without a value is independent of the group, which is how the select-all box works.
  • required on a single Checkbox uses the browser's validation: the form won't submit until it's checked.