Playground
Cards
Each option renders through the same internal card shell as CheckboxCard and SwitchCard: the whole card is the hit target, the radio circle is the control, and the item's label, descriptor and body fill the card's text slots. Every item requires a body.
<RadioCard label="How should we contact you?" defaultValue="email" items={[
{ value: 'email', label: 'Email', descriptor: 'Replies within one working day', body: 'We reply by the next working day.' },
{ value: 'phone', label: 'Phone', descriptor: 'Weekdays, 9am to 5pm', body: 'Call us for anything urgent.' },
{ value: 'post', label: 'Post', body: 'Letters take a little longer.', disabled: true },
]} />Sizes
Small
16px card padding, 20px radio circle.
<RadioCard size="sm" label="Small" items={ITEMS} />Medium
The default size. 16px padding, 24px circle.
<RadioCard size="md" label="Medium" items={ITEMS} />Hidden control
No radio circle
The radio circle is hidden and the card's own fill and edge carry the selected state. The input is still there, so the tab order, arrow keys and screen reader behaviour are unchanged, and the card spans the full width.
<RadioCard hideControl label="How should we contact you?" defaultValue="email" items={ITEMS} />States
Disabled card
A disabled card takes the disabled colours and ignores presses on the card body and the control. Arrow navigation skips it.
<RadioCard label="Contact preference" defaultValue="email" items={[
{ value: 'email', label: 'Email', body: 'Enabled.' },
{ value: 'phone', label: 'Phone', body: 'Disabled.', disabled: true },
]} />Disabled group
Disabling the group disables every card and removes them all from the tab order while keeping the group semantics.
<RadioCard label="Contact preference" defaultValue="email" disabled items={ITEMS} />When to use
Use a radio card group when each mutually exclusive option needs more room to explain itself than a single label line allows — a title, an optional second line, and a body line of context, inside one clearly-bounded target.
Because the whole card is the hit target, it suits consequential choices (a plan, a contact method, a delivery option) where the option’s consequences matter as much as its name.
When not to use
- A short list of simple options: use
Radio— a compact control-plus-text row is the right density. - Rich card content — links, buttons, images, anything interactive: not supported. The card is a single label/pressable, so nested interactive content would be invalid. The three text slots are strings for exactly this reason.
- Several independent booleans: use
CheckboxCard; a radio group is single-select. - A setting that applies immediately: use
SwitchCard.
Radio Card vs Radio
Both are one group: one selection, one tab stop, one accessible group name. RadioCard’s props are derived from RadioProps, so anything Radio accepts is accepted here too; only items changes shape (each card item requires label and body). The two components render through the same internal group core, so the selection, keyboard and tab-stop behaviour cannot drift.
Props
RadioCardProps = Omit<RadioProps, 'items'> & { items: RadioCardItem[] }.
| Prop | Type | Default | Description |
|---|---|---|---|
| items | RadioCardItem[] | - | Required. The options, in render order. Arrow navigation follows this order. Each item requires value, label and body. |
| value | string | - | Controlled selection. Omit and use defaultValue for uncontrolled usage. |
| defaultValue | string | - | Initial selection for uncontrolled usage. Ignored if value is provided. |
| onChange | (value: string) => void | - | Fires with the newly selected value, not the DOM event. |
| label | string | - | Rendered as the legend — the group accessible name. Web: a <legend>; native: a visible Text block. |
| descriptor | string | - | Optional secondary line beneath the legend. Wired to the fieldset via aria-describedby on web; not announced on native. |
| size | 'sm' | 'md' | 'md' | Card padding ramp and control size. Typography never changes with size. |
| hideControl | boolean | false | Hides the radio circle. The card fill and edge carry the selected state, and focus, keyboard and screen reader behaviour are unchanged. |
| disabled | boolean | false | Disables every card and removes them all from the tab order. |
| required | boolean | false | Web only. Propagated to each card input for native form validation. |
| name | string | - | Web only. The shared name every card input carries. Generated via useId() when omitted. |
| accessibilityLabel | string | - | Native only. Overrides the group accessible name outright. Falls back to label. |
| id | string | - | Web only. HTML id for the fieldset. Generated via useId() when omitted; derives the legend/descriptor ids and each card input id. |
| testID | string | - | Native only. Sub-ids: -legend, -item-{value} (control), -item-{value}-card, -item-{value}-card-header, -item-{value}-card-text (label and descriptor), -item-{value}-card-body, -item-{value}-dot. |
The card’s props are derived from RadioProps: Omit<RadioProps, 'items'> plus the card’s own items, so a prop added to Radio reaches RadioCard without a second edit. appearance is not accepted — the card is a component, not an appearance axis of Radio.
Items (RadioCardItem)
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | - | Required, and unique within the group. |
| label | string | - | Required. The card primary text line. |
| descriptor | string | - | Optional secondary line beneath the label, above the body. |
| body | string | - | Required. Body copy on its own full-width row beneath the label and control. |
| disabled | boolean | false | Disables this option only. Skipped by arrow navigation. |
A plain RadioItem is not accepted: body is required, so a card without one is a compile error rather than a runtime surprise.
Import
Web
import { RadioCard } from '@checkatrade/components-web';
import type { RadioCardItem, RadioCardProps } from '@checkatrade/components-web';Native
import { RadioCard } from '@checkatrade/components-native';
import type { RadioCardItem, RadioCardProps } from '@checkatrade/components-native';Basic usage
Controlled (both platforms):
const [value, setValue] = useState('email');
<RadioCard
label="How should we contact you?"
value={value}
onChange={setValue}
items={[
{ value: 'email', label: 'Email', body: 'We reply by the next working day.' },
{ value: 'phone', label: 'Phone', body: 'Call us for anything urgent.' },
]}
/>;Uncontrolled (both platforms):
<RadioCard
label="How should we contact you?"
defaultValue="email"
items={[
{ value: 'email', label: 'Email', body: 'We reply by the next working day.' },
{ value: 'phone', label: 'Phone', body: 'Call us for anything urgent.' },
]}
/> Platform status
| Platform / Area | Status |
|---|---|
| Design (Figma) | Caution |
| Web (React) | Beta |
| Native (React Native) | Beta |
| iOS (Swift) | Planned |
| Android (Kotlin) | Planned |
| Accessibility audit | Planned |
There is no Figma file for a radio card yet (DSK-96 is in progress), so the layout, selected treatment and required body line are the implementer’s reading of the existing card pattern, not a spec.
Accessibility
Web
- A real
<fieldset>+<legend>, so the group announces itself as a group with a name. - Each card is a
<label>wrapping the text block and the control, so the whole card is a click target. A real<input type="radio">inside it is the single focusable and semantic node, sharing onenamewith its siblings. - The card’s
labelis the accessible name and itsdescriptor+bodythe accessible description — announced separately, never concatenated. - The radio circle and dot are decorative (
aria-hidden="true"); state lives on the input. - One tab stop for the group: arrows move focus and selection together, wrapping and skipping disabled cards.
- A disabled group keeps its semantics while removing every card from the tab order.
React Native
- The container carries
accessibilityRole="radiogroup"and the group’s accessible name. - Each card’s radio control is a
PressablewithaccessibilityRole="radio",accessibilityState={{ checked, disabled }}and explicitaria-checked(react-native-web dropsaccessibilityState). - The card root is not an accessibility node and not a keyboard stop, so it adds no second announcement.
- Native has no description slot, so the name is the label, descriptor and body joined with a full stop.
- The whole card is the tap target. Pressing the text or the body selects the option exactly once.
Target size
The whole card is the target on both platforms, so the effective hit area is far larger than the 20–32px circle at every size.
Token gaps
The same gaps as Radio, which owns the circle and dot: no surface-tone-aware radio-checkbox/* tokens, and the dot’s diameter/inset have no fitting token. The card surface comes from the internal SelectableCard shell, so it also inherits that shell’s two gaps: native cannot apply the selector label tokens, and the selected border resolves to #040154 where Figma draws #0036fa. See the Checkbox Card page for both.
No releases yet.