Skip to content

Selection

Radio Card

Beta

A group of mutually exclusive options, each drawn as a selectable card, available on web (React) and native (React Native). One selection, one tab stop, one accessible group name — the card presentation of Radio.

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.

How should we contact you?
<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.

Small
<RadioCard size="sm" label="Small" items={ITEMS} />

Medium

The default size. 16px padding, 24px circle.

Medium
<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.

How should we contact you?
<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.

Contact preference
<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.

Contact preference
<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 / AreaStatus
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 one name with its siblings.
  • The card’s label is the accessible name and its descriptor + body the 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 Pressable with accessibilityRole="radio", accessibilityState={{ checked, disabled }} and explicit aria-checked (react-native-web drops accessibilityState).
  • 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.

Select a brand to demo:
Active brand: checkatrade