Checkbox Button Group
A multi-select group whose options are cards, built on a real checkbox group with keyboard selection and a per-item indicator.
Usage
Anatomy
Import the CheckboxButtonGroup component and access all parts using dot notation.
import { CheckboxButtonGroup } from "@blakeui/pro-react";
<CheckboxButtonGroup>
<CheckboxButtonGroup.Item>
<CheckboxButtonGroup.ItemIcon />
<CheckboxButtonGroup.ItemContent>
<CheckboxButtonGroup.ItemTitle />
<CheckboxButtonGroup.ItemDescription />
</CheckboxButtonGroup.ItemContent>
<CheckboxButtonGroup.Indicator />
</CheckboxButtonGroup.Item>
</CheckboxButtonGroup>;Controlled
Deploying to 1 region.
Custom Indicator
Disabled Group
Grid Layout
Icon Cards
No Indicator
Render Prop Children
A function child receives this item's live selection state, so the card can change its own content on selection without lifting value out of the group.
Subscription Plans
Prices exclude VAT. Change or cancel at any time.
Compare every featureWith Icons
With Ripple
Neither package ships a web ripple, so this composes the third-party m3-ripple — the same one the free Button docs reach for. It sits inside the item as a pointer-events: none overlay, so the press still lands on the checkbox.
CSS Classes
Base Class
.checkbox-button-group— The root, which is also the free.checkbox-group. It declares the three custom properties below, sets acalc(--spacing * 2)gap between label, description, and list, and carriescontainer-type: inline-sizeso the list can query it. An element can never match its own container query, which is why the containment sits here rather than on the list.
Layout Classes
.checkbox-button-group--flex— The default. The list stacks its cards in a column with a--spacing * 3gap..checkbox-button-group--grid— The list becomesdisplay: gridoverrepeat(var(--checkbox-button-group-columns), minmax(0, 1fr))with the same gap. Below a26remcontainer it collapses to a single column.
Element Classes
.checkbox-button-group__label— The group's<label>, with--spacing * 0.5of bottom margin..checkbox-button-group__description— The supporting line under the label..checkbox-button-group__list— The box holding the items, and the one the layout modifiers apply to..checkbox-button-group__item— One card, and also the free.checkbox. A relatively-positioned flex row aligned toflex-start,--spacing * 4of padding,1px solid --border,border-radius: var(--checkbox-button-group-item-radius), andbox-shadow: none— bordered-flat, like every card-family surface. Its rules are written as.checkbox-button-group .checkbox-button-group__itemso they match the free checkbox rules' specificity and win on source order; without that the card would centre its contents and pick up a 16px top margin from.checkbox-group [data-slot="checkbox"]..checkbox-button-group__indicator— The selection mark, absolutely positioned--spacing * 3.5from the top and the inline end..checkbox-button-group__item-content— The text column.flex: 1 1 autowith a--spacing * 1gap, and — only when the item actually has an indicator —padding-inline-end: var(--checkbox-button-group-indicator-gutter)..checkbox-button-group__item-title— 0.875rem / 1.25rem at weight 500 in--foreground..checkbox-button-group__item-description— 0.75rem / 1rem in--muted, withtext-wrap: pretty..checkbox-button-group__item-icon— A--spacing * 9square in--defaultatcalc(--radius * 2), holding a--spacing * 5glyph. It repaints to--accent/--accent-foregroundwhile the item is selected.
State Rules
[data-selected="true"]on the item —border-color: --accent,background-color: --accent-soft, andbox-shadow: inset 0 0 0 1px var(--accent). The inset shadow is what takes the ring from 1px to 2px without reflowing the card, and it is a stroke rather than elevation.[data-focus-visible="true"]/:focus-visibleon the item — a two-stop outer ring built from--ring-offset-width,--background, and--focus. A selected card that is also focused keeps both rings, so "chosen" and "here" stay separately readable.[data-disabled="true"]/[aria-disabled="true"]on the item —cursor: --cursor-disabled. The opacity andpointer-events: nonealready come from the free.checkbox, and the underlying control is a genuinelydisabledinput.[data-custom="true"]on the indicator — set whenIndicatoris given children. It starts atopacity: 0; scale: 0.8and reachesopacity: 1; scale: 1once[data-selected="true"]is also present, which is how a custom mark shows only while the item is selected.:is(:active, [data-pressed="true"])on the item —transition-duration: 0s, so the press-in snaps and only the release eases back on the base 150ms.
Reduced Motion
The indicator's scale is dropped — and its transition-property narrowed to opacity alone — under both [data-reduce-motion="true"] and @media (prefers-reduced-motion: reduce), the media half scoped :not(:is([data-reduce-motion="true"], [data-reduce-motion="true"] *)) so the two never double-apply. The fade survives in both: reduced motion removes the movement, not the transition. The card's own transition is colour and shadow only, so it has nothing to opt out of.
CSS Variables
--checkbox-button-group-item-radius— The card corner. Declared on the root asvar(--radius-2xl), the same radius the rest of the card family rounds to, which keeps the straight edge dominant on a short list row rather than reading as a pill. Note that@blakeui/stylesredefines--radius-2xlascalc(var(--radius) * 2)rather than Tailwind's fixed1rem, so it still tracks a theme that retunes the radius scale.--checkbox-button-group-columns— Column count for the grid layout. Declared as2on the root; thecolumnsprop sets it inline on the list.--checkbox-button-group-indicator-gutter— Room reserved at the end of the content column so a wrapping title clears the indicator. Declared ascalc(var(--spacing) * 7). Set it to0on a centred card, which reserves its indicator room at the top instead — the Icon Cards example above does exactly that.
Data Attributes
[data-slot="checkbox-button-group-indicator"],[data-slot="checkbox-button-group-item-content"],[data-slot="checkbox-button-group-item-title"],[data-slot="checkbox-button-group-item-description"],[data-slot="checkbox-button-group-item-icon"]— One per part, so any of them can be targeted without depending on its BEM class. The root and the item carry[data-slot="checkbox-group"]and[data-slot="checkbox"]from the free components they are built on.
API Reference
CheckboxButtonGroup
The root. A React Aria CheckboxGroup with role="group", so multi-selection, form value, and label association are handled for you.
| Prop | Type | Default | Description |
|---|---|---|---|
layout | "flex" | "grid" | "flex" | How the items are arranged. flex stacks them in a column; grid lays them out over columns tracks and collapses to one column below a 26rem container. |
columns | number | 2 | Column count, applied as an inline --checkbox-button-group-columns on the list. It is read only when layout is "grid"; passing it to a flex group is ignored rather than an error. |
label | ReactNode | — | The group's accessible name, rendered as a real label tied to the group. Omit it only when an aria-label names the group instead — a checkbox group with no name at all is a defect. |
description | ReactNode | — | A supporting line rendered under the label. |
value | string[] | — | The selected values, for a controlled group. Pair it with onChange. |
defaultValue | string[] | — | The initially selected values, for an uncontrolled group. |
onChange | (value: string[]) => void | — | Called with the full list of selected values whenever selection changes. |
isDisabled | boolean | false | Disables every item in the group. Each one becomes a genuinely disabled input, so it leaves the tab order and is announced as disabled. |
className | string | — | Merged onto .checkbox-button-group, alongside the layout modifier. Use labelClassName, descriptionClassName, and listClassName to reach the parts the root renders for you. |
children | ReactNode | — | The items. They are wrapped in .checkbox-button-group__list, which is the box the layout applies to. |
Also accepts every React Aria CheckboxGroup prop, including name, isRequired, isInvalid, and isReadOnly.
CheckboxButtonGroup.Item
One selectable card. A React Aria Checkbox, which renders a label around a visually-hidden input type="checkbox" — so the card's own text is its accessible name and Space toggles it.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | The value this card contributes to the group while it is selected. |
children | ReactNode | ((state) => ReactNode) | — | The card's content. Pass a function to receive this item's live state — isSelected, isHovered, isPressed, isFocusVisible, isDisabled, and the rest of React Aria's checkbox render props. |
isDisabled | boolean | false | Disables this card alone. It is skipped by the tab order and announced as disabled, because the underlying input really is disabled. |
className | string | — | Merged onto .checkbox-button-group__item. Tailwind utilities land in the utilities layer, so they win over the component's own rules — which is how the Icon Cards example turns the card into a centred column. |
Also accepts every React Aria Checkbox prop.
CheckboxButtonGroup.Indicator
The card's selection mark, pinned to the top-right. It is aria-hidden: the checked state already lives on the input, so the indicator's job is to keep selection from being carried by colour alone.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A custom mark, shown only while the item is selected. Left empty, the part renders the free checkbox — a control box that is visible at rest and fills in on selection. Omit the whole part for a card with no mark at all. |
className | string | — | Merged onto .checkbox-button-group__indicator. |
Also accepts every native span attribute.
CheckboxButtonGroup.ItemContent
The card's text column.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Normally ItemTitle and ItemDescription, though a card that needs a price or a chip can put arbitrary markup here — the Subscription Plans example does. |
className | string | — | Merged onto .checkbox-button-group__item-content. |
Also accepts every native div attribute.
CheckboxButtonGroup.ItemTitle
The card's primary label. Rendered as a span, so it stays part of the checkbox's accessible name rather than introducing a heading.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The label text. |
className | string | — | Merged onto .checkbox-button-group__item-title. |
Also accepts every native span attribute.
CheckboxButtonGroup.ItemDescription
The card's supporting line. Also a span, and also part of the accessible name — which is why it should read as more of the option, not as separate help text.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The secondary text. It wraps with text-wrap: pretty rather than truncating. |
className | string | — | Merged onto .checkbox-button-group__item-description. |
Also accepts every native span attribute.
CheckboxButtonGroup.ItemIcon
The card's leading glyph square. aria-hidden, because the card is already named by its text and an icon that also announced would name the checkbox twice.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | The glyph. An svg is sized to --spacing * 5 automatically. |
className | string | — | Merged onto .checkbox-button-group__item-icon. |
Also accepts every native span attribute.
Cell Switch
A settings-row toggle built on the Switch — the whole row is the hit target, not just the control, and cells stack into a settings panel.
Drop Zone
A file drop target with a browse affordance, built on React Aria's DropZone and FileTrigger — pointer drag, keyboard drag and the file picker all reach the same list.