Selectable Cards
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.
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> );}Checkbox card
Section titled “Checkbox card”Use CheckboxCard when users can select multiple options independently.
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" /> );}With image
Section titled “With image”Cards can include an image for visual identification. Pass an object with src and optional alt, loading, and aspectRatio properties.
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.
3 years extended warranty coverage
Heated seats, steering wheel, and windshield
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> );}Radio card
Section titled “Radio card”Use RadioCard within a RadioGroup for single-selection scenarios. The RadioGroup provides the shared name attribute and manages selection state.
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> );}Controlled and uncontrolled
Section titled “Controlled and uncontrolled”Uncontrolled
Section titled “Uncontrolled”By default, cards manage their own state. Use defaultChecked on CheckboxCard or defaultValue on RadioGroup to set the initial selection.
Controlled
Section titled “Controlled”For controlled behavior, use checked and onChange on CheckboxCard, or value and onChange on RadioGroup.
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> );}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> );}Interactive content
Section titled “Interactive content”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.
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> );}CSS-only usage
Section titled “CSS-only usage”For simple cases without interactive elements inside the card, use the .selectable-card CSS class with a <label> wrapper.
Content slots
Section titled “Content slots”The card uses a slot-based layout:
slot="title"– Primary labelslot="meta"– Secondary informationslot="description"– Additional detailsslot="image"– Optional image
<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>Radio group
Section titled “Radio group”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>With image
Section titled “With image”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>Accessibility
Section titled “Accessibility”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>(theRadioGroupcomponent handles this) - Provide meaningful
alttext for images, or usealt=""witharia-hidden="true"for decorative images
See the accessibility guidelines for detailed WCAG compliance information.
CheckboxCard
Section titled “CheckboxCard”| 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. | |||
RadioCard
Section titled “RadioCard”| 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> | - | - |
RadioGroup
Section titled “RadioGroup”| 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> | - | - |