CheckboxGroup

Lets users select any number of options from a visible list.

Installation

  1. Install the package.
npm install @hashintel/ds-components
  1. Import the component.
import { useState } from "react";

import { CheckboxGroup } from "@hashintel/ds-components";

export const EntityTypeFilter = () => {
  const [value, setValue] = useState<("person" | "company" | "project")[]>([
    "person",
  ]);

  return (
    <CheckboxGroup
      aria-label="Entity types"
      items={[
        { value: "person", label: "Person" },
        { value: "company", label: "Company" },
        { value: "project", label: "Project" },
      ]}
      value={value}
      onChange={setValue}
    />
  );
};

When to use

  • Do: Use CheckboxGroup when users can select any number of options — zero, one, or several — from a visible list.
  • Do: Use it for choices that apply on submit or an explicit confirmation or other scenarios where every option should stay visible so that users can review before committing.

When not to use

  • Don't: Don't use it when exactly one option must be chosen — use RadioGroup.
  • Don't: Don't use it for a single on/off choice — use Checkbox alone, or Toggle when the change applies immediately.
  • Don't: Avoid using it for long lists — beyond roughly 7 options, prefer Combobox or Select with multiple, which scale with search.

Examples

Basic

CheckboxGroup is controlled: value holds the selected option values and onChange receives the full next array — there is no uncontrolled mode. Each item takes a unique value plus any Checkbox props, such as label, disabled, or tone.

Disabled

Set disabled on the group to lock every option, or disabled: true on an item to lock just that one. Explain why in nearby text or a tooltip.

Invalid

Set invalid when validation fails — for example, nothing checked where at least one option is needed. Pair it with an error message; the red border alone doesn't say what's wrong.

Layouts

block (the default) stacks options for easy scanning. Use blockWithBorder for settings-style lists where each option reads as a row, and inline for 2–3 short options where vertical space is tight.

Sizes

size scales the boxes, labels, and spacing together, from xxs to lg; md is the default. Match the size of the surrounding form inputs.

Label placement

Set labelPlacement: "left" on items to put the label before the box.

Max selectable

maxSelectable caps how many options can be checked. At the cap, unchecked options disable; checked ones stay enabled so users can swap a selection. State the limit in the group's description — disabled boxes don't explain themselves.

Required

Set required to block native form submission until at least one option is checked: while the selection is empty every option is marked required and the group gets aria-required; checking any option clears the constraint.

With a group label

Wrap the group in Form.Field with as="legend" to give it a visible label, description, and error messages — as="legend" names the whole group rather than a single input.

Notify me aboutSelect all that apply

Behavior

  • onFocus and onBlur fire only when focus enters or leaves the group as a whole — moving between options doesn't retrigger them.
  • Options share one name for form submission, auto-generated unless you pass one. In a native form post, read the selection with formData.getAll(name).
  • autoFocus focuses the first option on mount; "never" also opts the group out of a containing dialog's or popover's initial focus.

Content

  • Write option labels as short, parallel phrases — sentence case, under 4 words, and with no end punctuation.
  • Phrase options positively — "Send weekly digest", never "Don't send weekly digest" — unchecking a negative is a double-negative.
  • Order options by likely frequency of use, or alphabetically when there's no natural order.
  • When multi-selection isn't obvious, say so in the group's description: "Select all that apply".

Accessibility

Each option implements the WAI-ARIA checkbox pattern, grouped under a single role="group" container. Every option is a native checkbox input wrapped in its label, so label clicks toggle them.

What you must do

  • Always name the group: a visible label via Form.Field with as="legend", or aria-label when the surrounding context already says what the list is.
  • Explain selection caps and disabled options in nearby text — a disabled checkbox doesn't say why it's unusable.
  • Pair invalid with an error message, e.g. Form.Field's errors — the red border alone isn't perceivable by everyone.

API reference

PropTypeDescription
items
(Omit<{ label?: ReactNode; labelPlacement?: "left" |⁠ "right" |⁠ undefined; labelAlign?: "top" |⁠ "center" |⁠ undefined; tone?: "neutral" |⁠ "brand" |⁠ "success" |⁠ undefined; indeterminate?: boolean |⁠ undefined; htmlValue?: string |⁠ undefined; } & { ...; } & SharedInputAndFieldProps & AriaAttributes, "size" |⁠ ... 4 more ....
value
NoInfer<ValueType>[]
onChange
(value: NoInfer<ValueType>[]) => void
layout
"inline" |⁠ "block" |⁠ "blockWithBorder"
default: "block"
maxSelectable
number
size
"xxs" |⁠ "xs" |⁠ "sm" |⁠ "md" |⁠ "lg"
default: "md"
disabled
boolean
autoFocus
boolean |⁠ "never"
onFocus
FocusEventHandler<HTMLInputElement>
onBlur
FocusEventHandler<HTMLInputElement>
required
boolean
name
string
invalid
boolean

Was this page helpful?

On this page