Card Select
The CardSelect component provides an accessible, customizable card-based selection interface. It supports both single and multiple selection modes with flexible layouts (horizontal, vertical, or grid), making it suitable for forms, settings, and decision workflows.
Variations
Selection modes
The CardSelect supports two selection types. single behaves like a radio group; multiple behaves like a set of checkboxes.
Layout
Cards can be arranged horizontally, vertically, or in a grid:
Gap
Control the gap between cards using the gap prop:
Icons and logos
Cards can display either an icon or a logo. If both are set, the icon wins:
Disabled
Cards can be disabled either individually or as a group:
Coming soon
Cards can be marked as not-yet-available. comingSoon forces the item disabled and shows a badge:
Glow
Selected cards can show an accent glow ring in addition to the standard checked border:
Installation
import { CardSelect } from "@harnessio/ui/components";
return ( <CardSelect.Root type="single"> <CardSelect.Item value="option-1" icon="git-branch"> <CardSelect.Title>Option 1</CardSelect.Title> <CardSelect.Description>With an icon</CardSelect.Description> </CardSelect.Item>
<CardSelect.Item value="option-2" logo="jira"> <CardSelect.Title>Option 2</CardSelect.Title> <CardSelect.Description>With a logo</CardSelect.Description> </CardSelect.Item> </CardSelect.Root>)Anatomy
CardSelect is a compound component. A CardSelect.Root holds the selection state and lays out any number of CardSelect.Item cards; each card holds a CardSelect.Title and, optionally, a CardSelect.Description.
<CardSelect.Root type="single"> <CardSelect.Item value="a" icon="git-branch"> <CardSelect.Title>Option A</CardSelect.Title> <CardSelect.Description>Supporting detail</CardSelect.Description> </CardSelect.Item> <CardSelect.Item value="b" logo="gitlab"> <CardSelect.Title>Option B</CardSelect.Title> </CardSelect.Item></CardSelect.Root>- Root — a
<div>that owns the selection value and shares it with the items via context. It sets the group role (radiogroupfortype="single",groupfortype="multiple"), and applies thelayoutandgapvariants. Forlayout="grid",cols/rowsare passed through as CSS custom properties. Root works controlled (value+onValueChange) or uncontrolled (defaultValue). - Item — the selectable card. It renders as a
<label>acting as the control itself:role="radio"orrole="checkbox"depending on the grouptype, witharia-checked/aria-disabledand its own keyboard handling. It contains an optionaliconorlogo(icon wins if both are given), the title/description, a “Coming Soon” badge whencomingSoonis set, and a check mark when selected. A visually hidden native<input>inside the label carries thename/value/checkedso the selection participates in form submission. - Title — a
<div>for the card’s heading. - Description — a
<div>for supporting text beneath the title.
A few behaviors worth knowing:
- Selecting works like the native analog:
singlekeeps exactly one item checked;multipletoggles items in and out of an array. - By default
singleis “sticky” — re-clicking the checked item keeps it selected. Setdeselectableon Root to let a second click clear the selection (it firesonValueChange(null)). comingSoonoverridesdisabled: the item is always non-selectable and shows the badge.glowonly renders its ring while the item is checked.
Best practices
Do
- Use
type="single"when exactly one choice is required, andtype="multiple"when several can be picked at once. - Keep each card’s title to a short, scannable label and use the description for one line of supporting detail.
- Give the group a visible heading or label nearby so people know what they’re choosing (see the accessibility note — the group has no built-in label).
- Use
comingSoonto preview options that aren’t available yet, rather than hiding them, so users know they’re on the roadmap.
Don’t
- Don’t mix icons and logos arbitrarily across cards in the same group — keep the visual treatment consistent.
- Don’t use Card Select for long lists of options; a Select or Multi Select scales better past a handful of choices.
- Don’t rely on the glow or check mark alone to convey selection — keep the labels and states clear for everyone.
Content
- Write titles as concise option names, not sentences (“Kubernetes”, “Amazon ECS”).
- Use the description for a single clarifying line; move longer explanations elsewhere.
- Use sentence case for descriptions, and keep parallel structure across cards in a group.
Accessibility
CardSelect ships selection semantics out of the box: the group carries a radiogroup/group role and each card exposes a radio/checkbox role with its checked and disabled state.
Keyboard
- Each enabled item is in the tab order (
tabindex="0"); disabled items are removed from it (tabindex="-1"). - Space or Enter on a focused item selects it (or toggles it, in
multiplemode).
Labeling
- Each item’s accessible name comes from the text inside it — the
TitleandDescriptionrender within the<label>, so screen readers announce them as the option’s name. - The check mark, glow ring, and “Coming Soon” badge are visual affordances; state is also conveyed through
aria-checked,aria-disabled, and the badge text. - Icons and logos are decorative next to the text label.
Focus & states
- Items show the standard keyboard focus ring, and selected/disabled/coming-soon states each have distinct styling in addition to their ARIA attributes.
- The group tracks a single roving selection for
singleand an array formultiple; the underlying hidden inputs share a commonnameso the choice submits with a form.
Known limitations
- The group has no built-in accessible name.
CardSelect.Rootrenders theradiogroup/groupelement but doesn’t accept or forwardaria-label/aria-labelledby, so the group itself is unlabeled. Provide a visible heading and associate it yourself (e.g. wrap the group in a labelledfieldset/legend) so assistive tech announces what the choice is for. - No arrow-key navigation between options. Unlike a native radio group, every enabled card is a tab stop and there’s no arrow-key roving — you move between options with Tab, not the arrow keys. This departs from the usual radio-group interaction pattern.
- Extra props land on the hidden input, not the card.
CardSelect.Itemspreads any extra HTML attributes onto the visually hidden,aria-hidden<input>rather than the visible<label>, soaria-*oridpassed toItemwon’t reach the element assistive tech actually reads. Put descriptive text in theTitle/Descriptioninstead.
Properties
Root
The CardSelect.Root is the top-level wrapper that controls selection state.
Prop | Required | Default | Type |
|---|---|---|---|
| type | true | 'single' | 'multiple' | |
| value | false | undefined | [] | unknown | unknown[] |
| defaultValue | false | undefined | unknown | unknown[] |
| onValueChange | false | undefined | (val: unknown) => void | (val: unknown[]) => void |
| layout | false | 'vertical' | 'horizontal' | 'vertical' | 'grid' |
| gap | false | 'md' | 'xs' | 'sm' | 'md' | 'lg' |
| rows | false | undefined | number |
| cols | false | undefined | number |
| disabled | false | false | boolean |
| deselectable | false | false | boolean |
| name | false | string |
Item
Represents an individual selectable card option.
Prop | Required | Default | Type |
|---|---|---|---|
| value | true | string | |
| icon | false | IconV2NamesType | |
| logo | false | LogoV2NamesType | |
| disabled | false | false | boolean |
| glow | false | false | boolean |
| comingSoon | false | false | boolean |
Any other HTML attributes passed to CardSelect.Item are forwarded to the internal visually hidden <input>, not the visible card — see the accessibility notes.
Title
Displays the main heading for a card item.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode |
Description
Provides additional details for a card item.
Prop | Required | Default | Type |
|---|---|---|---|
| children | true | ReactNode |