Skip to content

Selectable Cards

Edit on GitHub
@volvo-cars/css v2.3.0@volvo-cars/react-forms v2.0.0

Selectable cards provide a visually rich way to present choices that include images, descriptions, and metadata. Use CheckboxCard for multiple selections or RadioCard for single selection within a group.

Select trim level
550 000 kr

Base configuration with essential features

625 000 kr

Enhanced comfort and convenience features

750 000 kr

Premium features and advanced technology

import { RadioCard, RadioGroup } from '@volvo-cars/react-forms';
export function RadioCardGroup() {
return (
<RadioGroup name="trim" legend="Select trim level" defaultValue="core">
<RadioCard
value="core"
title="Core"
meta="550 000 kr"
description="Base configuration with essential features"
/>
<RadioCard
value="plus"
title="Plus"
meta="625 000 kr"
description="Enhanced comfort and convenience features"
/>
<RadioCard
value="ultra"
title="Ultra"
meta="750 000 kr"
description="Premium features and advanced technology"
/>
</RadioGroup>
);
}

Use CheckboxCard when users can select multiple options independently.

12 000 kr

3 years extended warranty coverage

import { CheckboxCard } from '@volvo-cars/react-forms';
export function BasicCheckboxCard() {
return (
<CheckboxCard
name="warranty"
title="Extended warranty"
meta="12 000 kr"
description="3 years extended warranty coverage"
/>
);
}

Cards can include an image for visual identification. Pass an object with src and optional alt, loading, and aspectRatio properties.

EX90
8 500 kr

Heated seats, steering wheel, and windshield

import { CheckboxCard } from '@volvo-cars/react-forms';
export function CheckboxCardWithImage() {
return (
<CheckboxCard
name="package"
title="Winter package"
meta="8 500 kr"
description="Heated seats, steering wheel, and windshield"
image={{
src: '/images/cars/thumbnails/ex90.avif',
alt: 'EX90',
}}
/>
);
}

Stack multiple checkbox cards using the stack-16 utility class.

12 000 kr

3 years extended warranty coverage

8 500 kr

Heated seats, steering wheel, and windshield

15 000 kr

Bowers & Wilkins premium audio

import { CheckboxCard } from '@volvo-cars/react-forms';
export function CheckboxCardGroup() {
return (
<div className="stack-16">
<CheckboxCard
name="extended-warranty"
title="Extended warranty"
meta="12 000 kr"
description="3 years extended warranty coverage"
/>
<CheckboxCard
name="winter-package"
title="Winter package"
meta="8 500 kr"
description="Heated seats, steering wheel, and windshield"
defaultChecked
/>
<CheckboxCard
name="premium-sound"
title="Premium sound system"
meta="15 000 kr"
description="Bowers & Wilkins premium audio"
/>
</div>
);
}

Use RadioCard within a RadioGroup for single-selection scenarios. The RadioGroup provides the shared name attribute and manages selection state.

Select wheels
18 inch aero wheels
Included
20 inch spoke wheels
4 500 kr
import { RadioCard, RadioGroup } from '@volvo-cars/react-forms';
export function RadioCardWithImage() {
return (
<RadioGroup name="wheels" legend="Select wheels" defaultValue="18-spoke">
<RadioCard
value="18-spoke"
title='18" 8-spoke aero wheels'
meta="Included"
image={{
src: '/images/wheels.webp',
alt: '18 inch aero wheels',
}}
/>
<RadioCard
value="20-spoke"
title='20" 5-spoke wheels'
meta="4 500 kr"
image={{
src: '/images/wheels.webp',
alt: '20 inch spoke wheels',
}}
/>
</RadioGroup>
);
}

By default, cards manage their own state. Use defaultChecked on CheckboxCard or defaultValue on RadioGroup to set the initial selection.

For controlled behavior, use checked and onChange on CheckboxCard, or value and onChange on RadioGroup.

12 000 kr

3 years extended warranty coverage

Selected: No

import { CheckboxCard } from '@volvo-cars/react-forms';
import { useState } from 'react';
export function ControlledCheckboxCard() {
const [checked, setChecked] = useState(false);
return (
<div className="stack-16">
<CheckboxCard
name="warranty"
title="Extended warranty"
meta="12 000 kr"
description="3 years extended warranty coverage"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
<p className="text-secondary">Selected: {checked ? 'Yes' : 'No'}</p>
</div>
);
}
Select trim level
550 000 kr
625 000 kr
750 000 kr

Selected: plus

import { RadioCard, RadioGroup } from '@volvo-cars/react-forms';
import { useState } from 'react';
export function ControlledRadioGroup() {
const [value, setValue] = useState('plus');
return (
<div className="stack-16">
<RadioGroup
name="trim-controlled"
legend="Select trim level"
value={value}
onChange={(e) => setValue(e.target.value)}
>
<RadioCard value="core" title="Core" meta="550 000 kr" />
<RadioCard value="plus" title="Plus" meta="625 000 kr" />
<RadioCard value="ultra" title="Ultra" meta="750 000 kr" />
</RadioGroup>
<p className="text-secondary">Selected: {value}</p>
</div>
);
}

When you need interactive elements (buttons, links) inside the card, use the React components. Interactive elements inside a CSS-only <label> wrapper are not accessible.

9 500 kr

Extends your warranty coverage to 5 years or 150,000 km

import { CheckboxCard } from '@volvo-cars/react-forms';
export function CardWithInteractiveContent() {
return (
<CheckboxCard
name="extended-warranty"
title="Extended warranty"
meta="9 500 kr"
description="Extends your warranty coverage to 5 years or 150,000 km"
>
<button type="button" className="link-underlined">
Learn more about coverage
</button>
</CheckboxCard>
);
}

For simple cases without interactive elements inside the card, use the .selectable-card CSS class with a <label> wrapper.

The card uses a slot-based layout:

  • slot="title" – Primary label
  • slot="meta" – Secondary information
  • slot="description" – Additional details
  • slot="image" – Optional image
volvocars.com
<label class="selectable-card">
<input type="checkbox" />
<div
slot="image"
class="bg-feedback-gray flex items-center justify-center h-full"
>
<code>image</code>
</div>
<code slot="title">title</code>
<code slot="meta">meta</code>
<code slot="description">description</code>
</label>

For radio buttons, give all inputs the same name attribute.

<div class="stack-16">
<label class="selectable-card">
<input type="radio" name="trim" checked />
<span slot="title">Core</span>
<span slot="meta">550 000 kr</span>
<div slot="description">
<span>Base configuration with essential features</span>
</div>
</label>
<label class="selectable-card">
<input type="radio" name="trim" />
<span slot="title">Plus</span>
<span slot="meta">625 000 kr</span>
<div slot="description">
<span>Enhanced comfort and convenience features</span>
</div>
</label>
<label class="selectable-card">
<input type="radio" name="trim" />
<span slot="title">Ultra</span>
<span slot="meta">750 000 kr</span>
<div slot="description">
<span>Premium features and advanced technology</span>
</div>
</label>
</div>

Add an <img> with slot="image" for visual content. The image stacks above content at narrow widths.

<label class="selectable-card">
<input type="checkbox" />
<img
class="aspect-16/9"
slot="image"
src="/images/cars/thumbnails/ex90.avif"
alt=""
aria-hidden="true"
/>
<span slot="title">Winter package</span>
<span slot="meta">8 500 kr</span>
<div slot="description">
<span>Heated seats, steering wheel, and windshield</span>
</div>
</label>

Cards provide accessible labels through the semantic relationship between the <label> (or React component) and the input. The entire card is clickable, providing a large touch target.

Key considerations:

  • Do not place interactive elements (links, buttons) inside CSS-only cards wrapped in <label> – use the React components instead
  • For radio groups, wrap cards in a <fieldset> with <legend> (the RadioGroup component handles this)
  • Provide meaningful alt text for images, or use alt="" with aria-hidden="true" for decorative images

See the accessibility guidelines for detailed WCAG compliance information.

Prop Type Required Default
name string - -
The name of the input to use when submitting the form.
value string - on
The value of the checkbox that will be submitted with the form data. @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox#value
disabled boolean - false
Whether the input is disabled.
required boolean - false
Whether the input is required.
onInvalid FormEventHandler<HTMLInputElement> - -
Fires if the input fails validation on form submit.
onFocus FocusEventHandler<HTMLInputElement> - -
Focus event handler.
onBlur FocusEventHandler<HTMLInputElement> - -
Blur event handler.
aria-label string - -
ARIA label for the input.
aria-describedby string - -
ARIA describedby for the input.
aria-invalid boolean - -
ARIA invalid state.
title string -
Main title or label for the card.
meta string - -
Optional metadata (e.g., pricing, status).
description string - -
Optional description text.
image { src: string; alt?: string; loading?: "lazy" |... - -
Optional image configuration. Can be either an object with image properties or a custom React element (e.g., Next.js Image). When using a custom element, ensure it respects the 52px height constraint applied by CSS.
children ReactNode - -
Custom content that renders below the description. Use this for advanced layouts or interactive elements.
aria-labelledby string - -
ARIA labelledby for the input. Only use this when you need to reference multiple elements for the label. The native <label> element already provides proper association.
hidden boolean - -
id string - -
dir string - -
lang string - -
slot string - -
translate "yes" | "no" - -
className string - -
style CSSProperties - -
tabIndex number - -
onPointerDown PointerEventHandler<Element> - -
onPointerEnter PointerEventHandler<Element> - -
onPointerLeave PointerEventHandler<Element> - -
onPointerMove PointerEventHandler<Element> - -
onPointerUp PointerEventHandler<Element> - -
checked boolean - -
Whether the checkbox is checked. Makes the input controlled.
onChange ChangeEventHandler<HTMLInputElement, Element> - -
Fires when the input is checked or unchecked.
defaultChecked boolean - -
Whether an (uncontrolled) checkbox is checked by default.
Prop Type Required Default
value string - on
The value that will be submitted with the form data if the radio is selected. @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/radio#data_representation_of_a_radio_group
disabled boolean - -
Whether the input is disabled.
onInvalid FormEventHandler<HTMLInputElement> - -
Fires if the input fails validation on form submit.
onFocus FocusEventHandler<HTMLInputElement> - -
Focus event handler.
onBlur FocusEventHandler<HTMLInputElement> - -
Blur event handler.
aria-label string - -
ARIA label for the input.
aria-describedby string - -
ARIA describedby for the input.
aria-invalid boolean - -
ARIA invalid state.
title string -
Main title or label for the card.
meta string - -
Optional metadata (e.g., pricing, status).
description string - -
Optional description text.
image { src: string; alt?: string; loading?: "lazy" |... - -
Optional image configuration. Can be either an object with image properties or a custom React element (e.g., Next.js Image). When using a custom element, ensure it respects the 52px height constraint applied by CSS.
children ReactNode - -
Custom content that renders below the description. Use this for advanced layouts or interactive elements.
aria-labelledby string - -
ARIA labelledby for the input. Only use this when you need to reference multiple elements for the label. The native <label> element already provides proper association.
hidden boolean - -
id string - -
dir string - -
lang string - -
slot string - -
translate "yes" | "no" - -
className string - -
style CSSProperties - -
tabIndex number - -
onPointerDown PointerEventHandler<Element> - -
onPointerEnter PointerEventHandler<Element> - -
onPointerLeave PointerEventHandler<Element> - -
onPointerMove PointerEventHandler<Element> - -
onPointerUp PointerEventHandler<Element> - -
Prop Type Required Default
legend ReactNode -
The legend to show above the radio group.
name string -
The name of the radios to use when submitting the form.
hint ReactNode - -
Additional hint or description for the entire radio group.
children ReactNode -
The radios.
required boolean - -
Makes the radio group required.
onChange ChangeEventHandler<HTMLInputElement, Element> - -
Fires when a radio is selected.
onInvalid FormEventHandler<HTMLFieldSetElement> - -
Fires if the radio group fails validation on form submit.
disabled boolean - false
Disable all radios in the group. Use sparingly as it can be non-obvious to users why an input has been disabled. Prefer showing validation messages and hints instead.
readOnly boolean - false
Makes the radios in the group read-only. Use sparingly, it's often preferred to present the data as regular text or in a table instead.
form string - -
Id of a form element that the radios should be associated with. Defaults to the containing form element.
enterKeyHint "done" | "go" | "next" | "previous" | "search" ... - -
errorMessage string - -
Set the error message of a radio group and mark it invalid.
aria-invalid boolean - false
Force the radio group to be invalid.
value string - -
Select the radio with this value. Either `value` or `defaultValue` must be set. Makes the radio group controlled.
defaultValue string - -
Select the uncontrolled radio with this value by default. Either `defaultValue` or `value` must be set.
hidden boolean - -
id string - -
title string - -
dir string - -
lang string - -
slot string - -
translate "yes" | "no" - -
className string - -
style CSSProperties - -
tabIndex number - -
onPointerDown PointerEventHandler<Element> - -
onPointerEnter PointerEventHandler<Element> - -
onPointerLeave PointerEventHandler<Element> - -
onPointerMove PointerEventHandler<Element> - -
onPointerUp PointerEventHandler<Element> - -