CheckboxGroup
Lets users select any number of options from a visible list.
Installation
- Install the package.
npm install @hashintel/ds-components- 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.
Behavior
onFocusandonBlurfire only when focus enters or leaves the group as a whole — moving between options doesn't retrigger them.- Options share one
namefor form submission, auto-generated unless you pass one. In a native form post, read the selection withformData.getAll(name). autoFocusfocuses 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.Fieldwithas="legend", oraria-labelwhen 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
invalidwith an error message, e.g.Form.Field'serrors— the red border alone isn't perceivable by everyone.
API reference
| Prop | Type | Description |
|---|---|---|
| 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>[] | The input value |
| onChange | (value: NoInfer<ValueType>[]) => void | The onChange handler |
| layout | "inline" | "block" | "blockWithBorder" default: "block" | How the options are arranged: block stacks them vertically, inline lays them out in a row, blockWithBorder stacks them inside a bordered container with a divider between options. |
| maxSelectable | number | |
| size | "xxs" | "xs" | "sm" | "md" | "lg" default: "md" | The size (height) of the element |
| disabled | boolean | Set to show the input as disabled |
| autoFocus | boolean | "never" | Set to true to make the element focused on mount or 'never' to prevent the item being auto-focused |
| onFocus | FocusEventHandler<HTMLInputElement> | The onFocus handler |
| onBlur | FocusEventHandler<HTMLInputElement> | The onBlur handler |
| required | boolean | Marks the input as required: sets the native required attribute, so assistive technology announces it and form validation blocks empty submits. Purely functional — the input looks unchanged; pair it with Form.Field's required for the visible mark. |
| name | string | The name of the input |
| invalid | boolean | Set to show the input as invalid |
Was this page helpful?