Developer Experience (DX)
Next, let's look at how developers use the Checkbox Group. It has one API, with the same names as the Checkbox and every other component, it's typed end to end, and it works with your forms however you build them.
import { useState } from "react";
import { CheckboxGroup } from "@/components/CheckboxGroup";
const emails = [
{ value: "news", label: "Product news" },
{ value: "tips", label: "Tips and guides" },
{ value: "events", label: "Events near you" },
];
export function Emails() {
const [picked, setPicked] = useState<string[]>([]);
return (
<CheckboxGroup name="emails" options={emails} value={picked} onValueChange={setPicked}>
Which emails would you like?
</CheckboxGroup>
);
}
Consistent
Once you've learned the Checkbox's API, you already know most of the Checkbox Group's. These names mean the same thing everywhere in Open Components:
| Name | Kind | What it means |
|---|---|---|
size | Prop | xs, sm, md, lg or xl, with the same text everywhere |
disabled | Prop | The component can't be used, and here, nothing inside it either |
name | Prop | The name its form sends it under, which every one of its boxes shares |
value | Prop | The checked options' values, which you bind the way your framework binds a form control's |
options | Prop | What people can pick from, each with a value and a label |
orientation | Prop | vertical or horizontal, the way its options run |
selectAll | Prop | The label of a box that checks or clears every option |
| Default slot | Slot | The legend, which names it, as the label names a Checkbox |
We recommend size="md" and orientation="vertical" as the defaults. The Checkbox Group also leaves a few things out, on purpose:
- There are no Checkboxes between its tags. The options are a prop, so the group can render a Checkbox for each one, give them all its
name, bind each one to its value and work out the select-all box for you, and a list of options from your data drops straight in. - There's no
required. HTML has no way to require one of a group of checkboxes, so the group leaves it to your code, as under Pick at least one. - There's no
hintorerror. Each of them is an element of its own, linked by ID, as on the Checkbox and the Input, so the group fits any layout. - There's no
colororvariant, since its boxes have none either.
Type-safe
Props, events and slots are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like orientation="row", which isn't one of the orientations, or a value that isn't a list.
| Prop | Type | Default | Description |
|---|---|---|---|
options | CheckboxGroupOption[] | One Checkbox for each, in this order | |
name | string | The name every box shares, which its form sends | |
value | string[] | [] | The checked options' values, when you bind it. Without it, the group keeps its own |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | How big its boxes, their labels and its legend are |
orientation | "vertical" | "horizontal" | "vertical" | Whether its options run in a column or in a row |
disabled | boolean | false | Disables every box in it, with <fieldset disabled> |
selectAll | string | The label of a select-all box above the options. Without it, there's none |
| Option field | Type | Description |
|---|---|---|
value | string | What the form sends while it's checked, and what the value lists |
label | string | The text that names its Checkbox |
| Event | Payload | Description |
|---|---|---|
change | Event | Fires when people check or uncheck any of its boxes, select-all box included |
It comes from the box that changed, and bubbles up to the <fieldset>, so one listener hears every box, and event.target is the one that changed. A binding to value follows the group for you, with the new list of values.
| Slot | Description |
|---|---|
| Default | The legend, which names the group |
We also export CheckboxGroupProps and CheckboxGroupOption, which come in handy for wrappers and options from your data:
import type { CheckboxGroupOption } from "@/components/CheckboxGroup";
const emails: CheckboxGroupOption[] = topics.map((topic) => ({ value: topic.id, label: topic.title }));
Composable
The Checkbox Group renders a <fieldset>, and everything you pass it that isn't a prop lands there, including id, class, ARIA attributes and event listeners. A ref to the Checkbox Group reaches the <fieldset> too, which is what you need to focus its first box when a submit fails.
Forms
Every box shares the group's name, and each checked box sends it with its own value, so the form sends one field with a value for each checked option, in the options' order, and nothing at all while none is checked. Read it with getAll(), since get() only returns the first value. That also means the form works before your JavaScript loads.
import type { FormEvent } from "react";
export function Preferences() {
function save(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
// One name, with a value for each checked option, so read them all.
api.savePreferences({ emails: data.getAll("emails") });
}
return (
<form onSubmit={save}>
<CheckboxGroup name="emails" options={emails} defaultValue={["news"]}>Which emails would you like?</CheckboxGroup>
<Button type="submit" color="primary">Save preferences</Button>
</form>
);
}
Keep in mind that a server reading the form sees emails=news&emails=tips, the same name once for each checked option, and no field at all when none is checked. Some frameworks keep only the last value of a repeated name unless you ask for all of them, like PHP, which needs the name to end in [], so check how yours reads it.
With other components
- Each option is a Checkbox, which sets its own variables on itself. To restyle every box in a group, set them from a class on the group with a descendant selector, like
.brand .checkbox { --checkbox--box--fill: … }, that gets past your framework's style scoping, as under Tokens. A value on the group itself doesn't reach them. - The Field, on our Roadmap, will render a group's hint and error message, and set its
aria-describedbyandaria-invalid, as it will for the Checkbox and the Input. - A Button that submits the form goes after the group, and takes the same
size, so their text lines up.
Controllable
The Checkbox Group doesn't keep any state you can't reach. Its value is yours to bind, or to leave to the group, and every other state is a prop or an attribute that you control:
You picked: news
const [picked, setPicked] = useState(["news"]);
<CheckboxGroup name="emails" options={emails} value={picked} onValueChange={setPicked}>Which emails would you like?</CheckboxGroup>
<p>You picked: {picked.join(", ") || "nothing yet"}</p>
<CheckboxGroup name="contact" options={contact} defaultValue={["email"]}>How can we reach you?</CheckboxGroup>
| State | How you control it |
|---|---|
| Value | value, bound to your state, or left to the group |
| Select all | selectAll, with its checked and mixed states worked out from the value |
| Disabled | disabled, or a <fieldset disabled> around it |
| Invalid | aria-invalid, set while its error message shows (see Pick at least one) |
| Focus | A ref to its <fieldset>, and focus() on its first box |
When you bind value, the boxes always show the list you hold. If a change doesn't make it into your state, like an option people aren't allowed to drop, the box goes back as soon as it renders, and so does the select-all box. When you don't bind it, the group keeps its own value, and starts out from the one you give it.
DX Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
checkbox-group/shared-vocabulary | Must | Component | It uses the shared names: size, disabled, name, value, options, orientation, selectAll, the default slot for its legend and change | Open Components | Review |
checkbox-group/typed | Must | Component | Props, events and slots are typed, with unions rather than string, and the value is a list of strings | Open Components | Type check |
checkbox-group/composes-checkbox | Must | Component | Each option and the select-all box is a Checkbox, so every Checkbox rule holds for each of them | Open Components | Review |
checkbox-group/form-control | Must | Component | Every option shares one name, and its form sends a value for each checked option, in the options' order, and nothing for the select-all box | HTML | Unit test |
checkbox-group/controllable | Should | Component | value is a list that can be bound or left to the group, and a bound group always shows the value you hold | Open Components | Unit test |
checkbox-group/fieldset-attributes | Must | Component | Attributes, listeners and refs land on the <fieldset>, and each box's change bubbles up to it | Open Components | Unit test |
FAQ
name, binds each one to the value, keeps them in order and works out the select-all box from them, none of which you'd want to repeat on every box. A list of options also comes straight from your data, like a list of topics from your server.<fieldset>, its legend and a real checkbox for each option, so even before the page hydrates, people can check them and the form sends them. Only the select-all box has to wait: its mixed state is a property that only JavaScript can set, and checking every option from it takes a script, so until then it's a box that does nothing on its own.