Accordion
Compose AccordionDetails and AccordionSummary to create expandable content sections built on the native <details> and <summary> elements.
Exterior
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
Charging
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
Climate
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
import { AccordionDetails, AccordionSummary,} from '@volvo-cars/react-accordion';
export function BasicAccordion() { return ( <> {['Exterior', 'Charging', 'Climate'].map((item) => ( <AccordionDetails key={item}> <AccordionSummary>{item}</AccordionSummary> <p className="pb-24 text-secondary"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget. </p> </AccordionDetails> ))} </> );}Composition
Section titled “Composition”AccordionDetails wraps each section. Its first child should be an AccordionSummary — this becomes the toggle. Everything else is the collapsible content.
AccordionSummary renders a <summary> element with an implicit button role. It must not contain interactive children (links, buttons, checkboxes).
Icon placement
Section titled “Icon placement”Control where the chevron icon appears with iconPlacement. Use iconAlignment to adjust vertical alignment when labels wrap.
Icon at end (default)
The chevron appears after the label.
Icon at start
The chevron appears before the label.
import { AccordionDetails, AccordionSummary,} from '@volvo-cars/react-accordion';
export function IconPlacement() { return ( <> <AccordionDetails> <AccordionSummary iconPlacement="end"> Icon at end (default) </AccordionSummary> <p className="pb-24 text-secondary"> The chevron appears after the label. </p> </AccordionDetails> <AccordionDetails> <AccordionSummary iconPlacement="start">Icon at start</AccordionSummary> <p className="pb-24 text-secondary"> The chevron appears before the label. </p> </AccordionDetails> </> );}| Prop | Values | Default |
|---|---|---|
iconPlacement | 'start', 'end' | 'end' |
iconAlignment | 'top', 'center' | 'top' |
Controlled and uncontrolled
Section titled “Controlled and uncontrolled”Uncontrolled (default)
Section titled “Uncontrolled (default)”By default, the browser manages toggle state via native <details>. Use defaultOpen to start a section expanded:
<AccordionDetails defaultOpen> <AccordionSummary>Open by default</AccordionSummary> <p>Content</p></AccordionDetails>Controlled
Section titled “Controlled”Use open and onToggle for full control — useful for wizard-style flows or syncing with external state:
Step 1
Content for Step 1. Use the `open` and `onToggle` props to fully control which item is expanded.
Step 2
Content for Step 2. Use the `open` and `onToggle` props to fully control which item is expanded.
Step 3
Content for Step 3. Use the `open` and `onToggle` props to fully control which item is expanded.
import { AccordionDetails, AccordionSummary,} from '@volvo-cars/react-accordion';import { useState } from 'react';
export function ControlledAccordion() { const [openIndex, setOpenIndex] = useState(-1);
return ( <> {['Step 1', 'Step 2', 'Step 3'].map((label, i) => ( <AccordionDetails key={label} open={openIndex === i} onToggle={(open) => setOpenIndex(open ? i : -1)} > <AccordionSummary>{label}</AccordionSummary> <p className="pb-24 text-secondary"> Content for {label}. Use the `open` and `onToggle` props to fully control which item is expanded. </p> </AccordionDetails> ))} </> );}Exclusive mode
Section titled “Exclusive mode”Wrap items in AccordionController to allow only one open at a time. Set exclusive="until-lg" to make it exclusive on mobile/tablet but allow multiple on desktop.
Exterior
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
Charging
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
Climate
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
import { AccordionController, AccordionDetails, AccordionSummary,} from '@volvo-cars/react-accordion';
export function ExclusiveAccordion() { return ( <AccordionController exclusive> {['Exterior', 'Charging', 'Climate'].map((item) => ( <AccordionDetails key={item}> <AccordionSummary>{item}</AccordionSummary> <p className="pb-24 text-secondary"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget. </p> </AccordionDetails> ))} </AccordionController> );}Stepped Accordion
Section titled “Stepped Accordion”The Stepped Accordion organizes content into collapsible sections displayed vertically with a connecting left border. Only one step can be open at a time, guiding users through sequential information.
import { SteppedAccordion, SteppedAccordionDetails, SteppedAccordionSummary,} from '@volvo-cars/react-accordion';import { useState } from 'react';
export function BasicSteppedAccordion() { const [currentStep, setCurrentStep] = useState(0);
const steps = [ { id: 0, title: 'Step one', content: 'Content for step one.' }, { id: 1, title: 'Step two', content: 'Content for step two.' }, { id: 2, title: 'Step three', content: 'Content for step three.' }, ];
return ( <SteppedAccordion> {steps.map(({ id, title, content }) => ( <SteppedAccordionDetails key={id} open={currentStep === id} onToggle={() => setCurrentStep(id)} > <SteppedAccordionSummary>{title}</SteppedAccordionSummary> <div className="pb-8 px-24 text-secondary">{content}</div> </SteppedAccordionDetails> ))} </SteppedAccordion> );}The component uses three parts:
SteppedAccordion– Container that provides the vertical layout and left borderSteppedAccordionDetails– Wraps each step, controls open state via theopenpropSteppedAccordionSummary– The clickable header for each step
Controlled state
Section titled “Controlled state”The stepped accordion is always controlled. Track the current step with state and update it via the onToggle callback. An open step cannot be closed directly—opening another step closes the current one.
const [currentStep, setCurrentStep] = useState(0);
<SteppedAccordionDetails open={currentStep === 0} onToggle={() => setCurrentStep(0)}>Rich content
Section titled “Rich content”Add content inside each step after the summary. Use the standard padding classes for consistent spacing.
Immersive listening
Integrated speakers
Customisable audio
import { SteppedAccordion, SteppedAccordionDetails, SteppedAccordionSummary,} from '@volvo-cars/react-accordion';import { useState } from 'react';
export function SteppedAccordionWithContent() { const [currentStep, setCurrentStep] = useState(0);
const steps = [ { id: 0, title: 'Immersive listening', content: 'This advanced sound system employs 21 speakers and two amplifiers to deliver high-fidelity audio in 3D throughout the cabin.', }, { id: 1, title: 'Integrated speakers', content: 'With the new soundbar concept, a 1040W amplifier and nine high-performance speakers, this state-of-the-art system delivers immersive surround sound for everyone.', }, { id: 2, title: 'Customisable audio', content: 'Every aspect of the audio system is tuned to the interior. Select Dynamic, Soft or Voice mode to deepen your listening experience.', }, ];
return ( <SteppedAccordion> {steps.map(({ id, title, content }) => ( <SteppedAccordionDetails key={id} open={currentStep === id} onToggle={() => setCurrentStep(id)} > <SteppedAccordionSummary>{title}</SteppedAccordionSummary> <div className="pb-8 px-24 text-secondary">{content}</div> </SteppedAccordionDetails> ))} </SteppedAccordion> );}Accessibility
Section titled “Accessibility”Key consumer responsibilities from the WCAG audit:
- Accessible name — provide clear accordion header text. If using icons or non-text indicators, add
aria-labeloraria-labelledby. - No interactive children in summary —
<summary>has an implicitbuttonrole. Nesting links, buttons, or other interactive elements insideAccordionSummaryis invalid. - Expand/collapse via keyboard — handled natively (Space / Enter on
<summary>). Ensure users can tab in and out without getting trapped. - Consistent labeling — use the same naming pattern for accordion headers across your interface.
AccordionDetails
Section titled “AccordionDetails”| Prop | Type | Required | Default |
|---|---|---|---|
hidden | boolean | - | - |
id | string | - | - |
title | string | - | - |
dir | string | - | - |
lang | string | - | - |
slot | string | - | - |
translate | "yes" | "no" | - | - |
style | CSSProperties | - | - |
onClick | MouseEventHandler<HTMLDetailsElement> | - | - |
onPointerEnter | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerMove | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerDown | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerUp | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerLeave | PointerEventHandler<HTMLDetailsElement> | - | - |
children | ReactNode | ✓ | - |
| The content of the accordion. The first child should be the `AccordionSummary` component. | |||
className | string | - | - |
| Custom class name, merged with existing classes. | |||
onToggle | ((open: boolean) => void) | - | - |
| Called when the accordion is toggled. | |||
onAnimationFinish | ((open: boolean) => void) | - | - |
| Called when the toggle animation is finished. | |||
open | boolean | - | - |
| If set, the accordion will be controlled by the value of this prop. Must be used together with the `onToggle` prop. | |||
defaultOpen | boolean | - | - |
| If true, the accordion will be open by default but can then be toggled freely by the user. | |||
AccordionSummary
Section titled “AccordionSummary”| Prop | Type | Required | Default |
|---|---|---|---|
hidden | boolean | - | - |
id | string | - | - |
title | string | - | - |
dir | string | - | - |
lang | string | - | - |
slot | string | - | - |
translate | "yes" | "no" | - | - |
style | CSSProperties | - | - |
onFocus | FocusEventHandler<HTMLElement> | - | - |
onBlur | FocusEventHandler<HTMLElement> | - | - |
onClick | MouseEventHandler<HTMLElement> | - | - |
onKeyDown | KeyboardEventHandler<HTMLElement> | - | - |
onKeyUp | KeyboardEventHandler<HTMLElement> | - | - |
onPointerEnter | PointerEventHandler<HTMLElement> | - | - |
onPointerMove | PointerEventHandler<HTMLElement> | - | - |
onPointerDown | PointerEventHandler<HTMLElement> | - | - |
onPointerUp | PointerEventHandler<HTMLElement> | - | - |
onPointerLeave | PointerEventHandler<HTMLElement> | - | - |
children | ReactNode | ✓ | - |
| The label of the summary. Should not contain interactive elements. | |||
icon | ReactNode | - | - |
| The icon to display. | |||
iconPlacement | "start" | "end" | - | end |
| The placement of the icon - before or after the label. | |||
iconAlignment | "top" | "center" | - | top |
| The vertical alignment of the icon. | |||
className | string | - | - |
| Custom class name, merged with existing classes. | |||
AccordionController
Section titled “AccordionController”| Prop | Type | Required | Default |
|---|---|---|---|
exclusive | boolean | "until-lg" | - | true |
SteppedAccordion
Section titled “SteppedAccordion”| Prop | Type | Required | Default |
|---|---|---|---|
className | string | - | - |
onClick | MouseEventHandler<HTMLDivElement> | - | - |
onMouseOver | MouseEventHandler<HTMLDivElement> | - | - |
onMouseOut | MouseEventHandler<HTMLDivElement> | - | - |
style | CSSProperties | - | - |
SteppedAccordionDetails
Section titled “SteppedAccordionDetails”| Prop | Type | Required | Default |
|---|---|---|---|
open | boolean | ✓ | - |
| Whether the details are open. | |||
hidden | boolean | - | - |
id | string | - | - |
title | string | - | - |
dir | string | - | - |
lang | string | - | - |
slot | string | - | - |
translate | "yes" | "no" | - | - |
style | CSSProperties | - | - |
onClick | MouseEventHandler<HTMLDetailsElement> | - | - |
onPointerEnter | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerMove | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerDown | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerUp | PointerEventHandler<HTMLDetailsElement> | - | - |
onPointerLeave | PointerEventHandler<HTMLDetailsElement> | - | - |
children | ReactNode | ✓ | - |
| The content of the accordion. The first child should be the `AccordionSummary` component. | |||
className | string | - | - |
| Custom class name, merged with existing classes. | |||
onToggle | ((open: boolean) => void) | - | - |
| Called when the accordion is toggled. | |||
onAnimationFinish | ((open: boolean) => void) | - | - |
| Called when the toggle animation is finished. | |||
SteppedAccordionSummary
Section titled “SteppedAccordionSummary”| Prop | Type | Required | Default |
|---|---|---|---|
hidden | boolean | - | - |
id | string | - | - |
title | string | - | - |
dir | string | - | - |
lang | string | - | - |
slot | string | - | - |
translate | "yes" | "no" | - | - |
style | CSSProperties | - | - |
onFocus | FocusEventHandler<HTMLElement> | - | - |
onBlur | FocusEventHandler<HTMLElement> | - | - |
onClick | MouseEventHandler<HTMLElement> | - | - |
onKeyDown | KeyboardEventHandler<HTMLElement> | - | - |
onKeyUp | KeyboardEventHandler<HTMLElement> | - | - |
onPointerEnter | PointerEventHandler<HTMLElement> | - | - |
onPointerMove | PointerEventHandler<HTMLElement> | - | - |
onPointerDown | PointerEventHandler<HTMLElement> | - | - |
onPointerUp | PointerEventHandler<HTMLElement> | - | - |
onPointerLeave | PointerEventHandler<HTMLElement> | - | - |
children | ReactNode | ✓ | - |
| The label of the summary. Should not contain interactive elements. | |||
icon | ReactNode | - | - |
| The icon to display. | |||
iconPlacement | "start" | "end" | - | - |
| The placement of the icon - before or after the label. | |||
iconAlignment | "top" | "center" | - | - |
| The vertical alignment of the icon. | |||
className | string | - | - |
| Custom class name, merged with existing classes. | |||