Checkbox Group
Visible short-list multi choice with vertical or horizontal layout.
Checkbox Group shows a short list of options as always-visible checkboxes so users can turn several on at once. Pass the shared Option shape from Select (value, label, optional disable). Prefer it when every choice should stay on screen; reach for Multi-Select when the list is long, searchable, or async; use a single Checkbox for a boolean or consent field.
When To Use
- Pick several values from a short, always-visible list (notification channels, features, tags).
- Prefer Checkbox Group for ≤ ~7 independent options so users can scan without opening a popover.
- Use Multi-Select when the list is long, searchable, or async.
- Use Radio Group when the choice is mutually exclusive.
- Use a single Checkbox for a boolean or consent field (terms, marketing opt-in).
Interactions
| Event | Behavior |
|---|---|
| Check an enabled option | Calls onValueChange with that option’s value appended (SelectValue[]). |
| Uncheck an enabled option | Calls onValueChange with that value removed — including when the last option is cleared. |
orientation="horizontal" | Options lay out in a wrapping row instead of a vertical stack. |
Option with disable: true | That checkbox stays non-interactive; siblings remain selectable. |
Group disabled | Every option is non-interactive. |
Empty Value & Clearing
Unlike Radio Group, clearing is a first-class user action:
| Situation | Behavior |
|---|---|
| Empty selection | Always [] — never null. Omit value or pass [] for none checked. |
| Uncheck the last option | Emits []. Valid product state for “optional multi” fields. |
| Required “at least one” | Use form required (Formily treats [] as empty) or a custom validator. |
| vs Radio Group | Radios cannot uncheck to empty; Checkbox Group can. |
Do not model the field as null for empty — keep the type SelectValue[] end to end.
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/checkbox-group.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/checkbox-group.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/checkbox-group.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/checkbox-group.jsonOr with a namespace: npx shadcn@latest add @f-ui/checkbox-group.
The CLI pulls checkbox and label from the default shadcn registry, and select from f-ui for the shared Option / SelectValue types.
Usage
import { CheckboxGroup } from '@/components/f-ui/checkbox-group/checkbox-group';
const options = [
{ label: 'Email', value: 'email' },
{ label: 'SMS', value: 'sms' },
{ label: 'Push', value: 'push' },
];
<CheckboxGroup
aria-label="Notification channels"
options={options}
value={value}
onValueChange={setValue}
/>Examples
Notification Channels
Controlled vertical group for a small multi-select enum. Toggle options and watch the readout update.
Selected: email
"use client";
import { useState } from "react";
import { CheckboxGroup } from "@/components/f-ui/checkbox-group/checkbox-group";
const options = [
{ label: "Email", value: "email" },
{ label: "SMS", value: "sms" },
{ label: "Push", value: "push" },
];
export function CheckboxGroupDemo() {
const [value, setValue] = useState<(string | number)[]>(["email"]);
return (
<div className="space-y-3">
<CheckboxGroup
aria-label="Notification channels"
options={options}
value={value}
onValueChange={setValue}
/>
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium">
{value.length ? value.join(", ") : "none"}
</span>
</p>
</div>
);
}Horizontal Size
Use orientation="horizontal" for compact multi-choice that reads left-to-right (sizes, ratings, short codes).
"use client";
import { useState } from "react";
import { CheckboxGroup } from "@/components/f-ui/checkbox-group/checkbox-group";
export function CheckboxGroupHorizontalDemo() {
const [value, setValue] = useState<(string | number)[]>(["s", "m"]);
return (
<CheckboxGroup
aria-label="Size"
orientation="horizontal"
options={[
{ label: "S", value: "s" },
{ label: "M", value: "m" },
{ label: "L", value: "l" },
]}
value={value}
onValueChange={setValue}
/>
);
}Empty Selection
Uncheck every option — the value becomes [], shown in the readout. That is the group’s empty state (not null).
Selected: email
"use client";
import { useState } from "react";
import { CheckboxGroup } from "@/components/f-ui/checkbox-group/checkbox-group";
const options = [
{ label: "Email", value: "email" },
{ label: "SMS", value: "sms" },
{ label: "Push", value: "push" },
];
/**
* Empty selection is always `[]` (never `null`). Unchecking the last option
* is a first-class user action — unlike Radio Group.
*/
export function CheckboxGroupEmptyDemo() {
const [value, setValue] = useState<(string | number)[]>(["email"]);
return (
<div className="space-y-3">
<CheckboxGroup
aria-label="Notification channels"
options={options}
value={value}
onValueChange={setValue}
/>
<p className="text-muted-foreground text-xs">
Selected:{" "}
<span className="text-foreground font-medium">
{value.length ? value.join(", ") : "[] (empty)"}
</span>
</p>
</div>
);
}Disabled Options and Group
Per-option disable blocks one choice; group disabled locks the whole set.
"use client";
import { CheckboxGroup } from "@/components/f-ui/checkbox-group/checkbox-group";
export function CheckboxGroupDisabledDemo() {
return (
<div className="space-y-6">
<CheckboxGroup
aria-label="Plan with disabled option"
options={[
{ label: "Free", value: "free" },
{ label: "Pro", value: "pro" },
{ label: "Enterprise", value: "ent", disable: true },
]}
value={["free"]}
/>
<CheckboxGroup
aria-label="Disabled group"
disabled
options={[
{ label: "A", value: "a" },
{ label: "B", value: "b" },
]}
value={["a"]}
/>
</div>
);
}Edge Cases & Semantics
| Question | Answer |
|---|---|
| Empty value type? | [] only — not null. |
| Can the user clear all? | Yes — uncheck every option. |
| Selection order? | Values append in click order; uncheck removes that entry. Do not assume option-list order. |
Value not in options (stale)? | Stale entries stay in value until you filter them — the group only toggles known options. |
| Indeterminate / “select all” parent? | Not built in. Compose separately if you need bulk select. |
string vs number option values? | Supported; equality uses the original Option.value. |
| Standalone (no FormItem label)? | Pass aria-label on the group. |
| Formily kind name? | kind="checkboxGroup" + componentProps.options. See Form (Formily) — Choice Groups. |
| vs single Checkbox? | Single Checkbox is boolean / consent — not a multi-value list. |
vs Multi-Select / kind="multiSelect"? | Multi-Select for long / searchable / async / creatable lists with chips. |
Composition
CheckboxGroup
└── (per option) item row
├── Checkbox
└── LabelAPI Reference
Props
| Prop | Type | Default |
|---|---|---|
value | SelectValue[] | — |
onValueChange | (value: SelectValue[]) => void | — |
options | readonly Option[] | — |
orientation | 'vertical' | 'horizontal' | 'vertical' |
disabled | boolean | false |
id | string | auto |
className | string | — |
classNames | Partial<Record<CheckboxGroupSlot, string>> | — |
aria-label | string | — |
aria-invalid | boolean | — |
aria-required | boolean | — |
aria-describedby | string | — |
Option is { value: SelectValue; label: string; disable?: boolean } from Select. SelectValue is string | number.
When value is omitted, the group treats selection as [].
Slots
| Slot | Applied to |
|---|---|
root | Outer checkbox group container |
item | Each option row (Checkbox + Label) |
label | Option Label |