Skip to content

Radio Button

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

Radios allow the user to select a single option from a group. Use RadioGroup and Radio from @volvo-cars/react-forms, or build with plain HTML using native <input type="radio"> elements styled automatically by @volvo-cars/css.

Choose your terms
import { Radio, RadioGroup } from '@volvo-cars/react-forms';
export function RadioGroupBasic() {
return (
<RadioGroup name="terms" legend="Choose your terms" defaultValue="6000">
<Radio value="6000" label="6,000 km/yr" />
<Radio value="8000" label="8,000 km/yr" />
<Radio value="unlimited" label="Unlimited km/yr" />
</RadioGroup>
);
}

Both RadioGroup and individual Radio components accept a hint prop. The group hint describes the entire set, while per-radio hints add context to individual options. Hints are linked to inputs via aria-describedby.

Choose your mileage

Select the mileage for your subscription

500 km/month

667 km/month

No monthly cap

import { Radio, RadioGroup } from '@volvo-cars/react-forms';
export function RadioGroupWithHints() {
return (
<RadioGroup
name="mileage"
legend="Choose your mileage"
defaultValue="6000"
hint="Select the mileage for your subscription"
>
<Radio value="6000" label="6,000 km/yr" hint="500 km/month" />
<Radio value="8000" label="8,000 km/yr" hint="667 km/month" />
<Radio value="unlimited" label="Unlimited km/yr" hint="No monthly cap" />
</RadioGroup>
);
}

Pass defaultValue to RadioGroup to pre-select a radio without managing state — see the opening example above.

Pass value and onChange to RadioGroup when you need to read or drive the selection from state.

Powertrain
import { Radio, RadioGroup } from '@volvo-cars/react-forms';
import { useState } from 'react';
export function RadioGroupControlled() {
const [value, setValue] = useState('electric');
return (
<RadioGroup
name="powertrain"
legend="Powertrain"
value={value}
onChange={(e) => setValue(e.target.value)}
>
<Radio value="electric" label="Electric" />
<Radio value="hybrid" label="Hybrid" />
<Radio value="mild-hybrid" label="Mild hybrid" />
</RadioGroup>
);
}

Set errorMessage on RadioGroup to display an error and mark the group invalid. The error is hidden when the group is disabled. You can also force an invalid state with aria-invalid.

Choose your terms

Please choose an option

import { Radio, RadioGroup } from '@volvo-cars/react-forms';
export function RadioGroupError() {
return (
<RadioGroup
name="terms-error"
legend="Choose your terms"
errorMessage="Please choose an option"
>
<Radio value="6000" label="6,000 km/yr" />
<Radio value="8000" label="8,000 km/yr" />
<Radio value="unlimited" label="Unlimited km/yr" />
</RadioGroup>
);
}

RadioGroup renders a <fieldset> with a vertical stack layout. If you need a different layout — horizontal, grid, or integrated into a custom form — use RadioContextProvider directly. Wrap your Radio components in a RadioContextProvider with a name and either defaultValue or value/onChange, and supply your own <fieldset>.

import { Radio, RadioContextProvider } from '@volvo-cars/react-forms';
export function RadioCustomLayout() {
return (
<fieldset className="flex gap-16 flex-wrap">
<RadioContextProvider
value={{
defaultValue: 'hybrid',
name: 'engine',
}}
>
<Radio value="electric" label="Electric" />
<Radio value="hybrid" label="Hybrid" />
<Radio value="mild-hybrid" label="Mild hybrid" />
</RadioContextProvider>
</fieldset>
);
}

@volvo-cars/css styles native <input type="radio"> elements automatically — no class needed. Group radios inside a <fieldset> with flex-col and stack-16 for correct spacing.

Choose your model
<fieldset class="flex-col stack-16">
<legend class="mb-4 font-medium">Choose your model</legend>
<div class="flex-row">
<input type="radio" name="model" id="ex90" value="ex90" checked />
<label class="ml-8" for="ex90">Volvo EX90</label>
</div>
<div class="flex-row">
<input type="radio" name="model" id="xc90" value="xc90" />
<label class="ml-8" for="xc90">Volvo XC90</label>
</div>
<div class="flex-row">
<input type="radio" name="model" id="s90" value="s90" />
<label class="ml-8" for="s90">Volvo S90 Recharge</label>
</div>
</fieldset>

Wrap the label and hint in a flex-col stack-4 container. Use micro text-secondary for the hint text.

Choose your plan
Includes 5 GB storage
Includes 50 GB storage
Includes unlimited storage
<fieldset class="flex-col stack-16">
<legend class="mb-4 font-medium">Choose your plan</legend>
<div class="flex-row">
<input type="radio" name="plan" id="plan-basic" value="basic" />
<div class="flex-col stack-4 ml-8">
<label for="plan-basic">Basic</label>
<span class="micro text-secondary">Includes 5 GB storage</span>
</div>
</div>
<div class="flex-row">
<input
type="radio"
name="plan"
id="plan-standard"
value="standard"
checked
/>
<div class="flex-col stack-4 ml-8">
<label for="plan-standard">Standard</label>
<span class="micro text-secondary">Includes 50 GB storage</span>
</div>
</div>
<div class="flex-row">
<input type="radio" name="plan" id="plan-premium" value="premium" />
<div class="flex-col stack-4 ml-8">
<label for="plan-premium">Premium</label>
<span class="micro text-secondary">Includes unlimited storage</span>
</div>
</div>
</fieldset>

Set aria-invalid on the <fieldset> (not on individual radios). Include an error message with role="alert" linked via aria-describedby.

Choose your plan Please select a plan
<fieldset
class="flex-col stack-16"
aria-invalid="true"
aria-describedby="radio-error"
>
<legend class="font-medium mb-4">Choose your plan</legend>
<span role="alert" id="radio-error" class="text-feedback-red micro"
>Please select a plan</span
>
<div class="flex-row">
<input type="radio" name="plan-error" id="plan-error-a" value="basic" />
<label class="ml-8" for="plan-error-a">Basic</label>
</div>
<div class="flex-row">
<input type="radio" name="plan-error" id="plan-error-b" value="standard" />
<label class="ml-8" for="plan-error-b">Standard</label>
</div>
<div class="flex-row">
<input type="radio" name="plan-error" id="plan-error-c" value="premium" />
<label class="ml-8" for="plan-error-c">Premium</label>
</div>
</fieldset>

Adding any class to a radio input removes the default design system styles. To keep them while extending, include the radio class alongside your own.

<div class="flex gap-16 items-center">
<div class="flex-row">
<input
type="radio"
name="custom"
id="custom-default"
value="default"
checked
/>
<label class="ml-8" for="custom-default">Default (no class)</label>
</div>
<div class="flex-row">
<input
type="radio"
class="radio my-custom-style"
name="custom"
id="custom-extended"
value="extended"
/>
<label class="ml-8" for="custom-extended"
>Extended (radio + custom class)</label
>
</div>
</div>

Key responsibilities from the WCAG audit:

  • Label every radioRadio requires a label prop. For CSS-only, pair each <input> with a <label htmlFor>.
  • Group with a legend — always wrap radios in a <fieldset> with a <legend>. RadioGroup handles this automatically.
  • Error on the group — set aria-invalid and aria-describedby on the <fieldset>, not on individual radios. RadioGroup wires this when errorMessage is set.
  • Hint association — use aria-describedby to link hints to their inputs. Radio does this automatically when hint is provided.
  • Pointer target size — the radio control may be smaller than the recommended 44×44px target. The adjacent label extends the clickable area but verify it meets your requirements.
  • Status messages — VoiceOver in Safari has known issues announcing error messages on radio groups. Test with multiple screen readers.
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> - -
Prop Type Required Default
label string -
Concise label for the radio.
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
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> - -
disabled boolean - false
Disables the input. Use sparingly as it can be non-obvious to users why an input has been disabled. Prefer showing validation messages and hints instead.
autoFocus boolean - false
Gives the input focus on page load. Use sparingly as it can be confusing to screen-reader and mobile users.
readOnly boolean - false
Makes the input 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 this input should be associated with. Defaults to the containing form element.
hint ReactNode - -
Additional hint or description.
enterKeyHint "done" | "go" | "next" | "previous" | "search" ... - -
Hint the browser about what label to show for the Enter button on mobile keyboards.
required boolean - false
Makes the input required.
aria-describedby string - -
aria-labelledby string - -
onInvalid FormEventHandler<HTMLInputElement> - -
Fires if the input fails validation on form submit.
onFocus FocusEventHandler<HTMLInputElement> - -
onBlur FocusEventHandler<HTMLInputElement> - -
onKeyDown KeyboardEventHandler<HTMLInputElement> - -
onKeyUp KeyboardEventHandler<HTMLInputElement> - -