Accordion

Vertically stacked sections that expand to reveal content.

onChange called 0 time(s)

import { accordion } from '@markless/ui';export default function AccordionBasic() @{	const questions = [		{ value: 'ship', question: 'When does my order ship?', answer: 'Orders leave within two working days.' },		{ value: 'returns', question: 'How do I return something?', answer: 'Start a return from your orders page.' },		{ value: 'overseas', question: 'Do you ship overseas?', answer: 'Everywhere but Antarctica.' },	];	<accordion.root class="accordion" value="ship">		{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}		<div class="sections" role="presentation">			@for (const q of questions; key q.value) {				<accordion.item class="section" value={q.value}>					<accordion.itemlabel class="heading">						<accordion.itemtrigger class="trigger">{q.question}</accordion.itemtrigger>					</accordion.itemlabel>					<accordion.itemcontent class="panel">{q.answer}</accordion.itemcontent>				</accordion.item>			}		</div>	</accordion.root>}

Examples

With multiple, sections open on their own and value becomes the list of open ones. Opening the third here leaves the first two as they are.

import { accordion } from '@markless/ui';export default function AccordionMultiple() @{	const questions = [		{ value: 'ship', question: 'When does my order ship?', answer: 'Orders leave within two working days.' },		{ value: 'returns', question: 'How do I return something?', answer: 'Start a return from your orders page.' },		{ value: 'overseas', question: 'Do you ship overseas?', answer: 'Everywhere but Antarctica.' },	];	<accordion.root class="accordion" multiple value={['ship', 'returns']}>		{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}		<div class="sections" role="presentation">			@for (const q of questions; key q.value) {				<accordion.item class="section" value={q.value}>					<accordion.itemlabel class="heading">						<accordion.itemtrigger class="trigger">{q.question}</accordion.itemtrigger>					</accordion.itemlabel>					<accordion.itemcontent class="panel">{q.answer}</accordion.itemcontent>				</accordion.item>			}		</div>	</accordion.root>}

A disabled item's trigger is a disabled <button>, so the arrow-key walk steps straight past it and nothing has to remember to skip it.

import { accordion } from '@markless/ui';export default function AccordionDisabled() @{	const questions = [		{ value: 'ship', question: 'When does my order ship?', answer: 'Orders leave within two working days.', disabled: false },		{ value: 'trade', question: 'Trade accounts', answer: 'Sign in to a trade account to read this.', disabled: true },		{ value: 'overseas', question: 'Do you ship overseas?', answer: 'Everywhere but Antarctica.', disabled: false },	];	<accordion.root class="accordion" value="ship">		{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}		<div class="sections" role="presentation">			@for (const q of questions; key q.value) {				<accordion.item class="section" value={q.value} disabled={q.disabled}>					<accordion.itemlabel class="heading">						<accordion.itemtrigger class="trigger">{q.question}</accordion.itemtrigger>					</accordion.itemlabel>					<accordion.itemcontent class="panel">{q.answer}</accordion.itemcontent>				</accordion.item>			}		</div>	</accordion.root>}

value on the root names the section that is open, matched against each item's own value. Name a section and the accordion starts with that one showing.

import { accordion } from '@markless/ui';export default function AccordionValue() @{	const questions = [		{ value: 'ship', question: 'When does my order ship?', answer: 'Orders leave within two working days.' },		{ value: 'returns', question: 'How do I return something?', answer: 'Start a return from your orders page.' },	];	<accordion.root class="accordion" value="returns">		{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}		<div class="sections" role="presentation">			@for (const q of questions; key q.value) {				<accordion.item class="section" value={q.value}>					<accordion.itemlabel class="heading">						<accordion.itemtrigger class="trigger">{q.question}</accordion.itemtrigger>					</accordion.itemlabel>					<accordion.itemcontent class="panel">{q.answer}</accordion.itemcontent>				</accordion.item>			}		</div>	</accordion.root>}

A closed panel is hidden="until-found", so searching this page for Antarctica opens the section holding it. Setting disableUntilFound on the root hides closed panels outright instead.

import { accordion } from '@markless/ui';export default function AccordionFind() @{	const questions = [		{ value: 'ship', question: 'When does my order ship?', answer: 'Orders leave within two working days.' },		{ value: 'overseas', question: 'Do you ship overseas?', answer: 'Everywhere but Antarctica, which keeps losing our parcels.' },	];	<accordion.root class="accordion" value="ship">		{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}		<div class="sections" role="presentation">			@for (const q of questions; key q.value) {				<accordion.item class="section" value={q.value}>					<accordion.itemlabel class="heading">						<accordion.itemtrigger class="trigger">{q.question}</accordion.itemtrigger>					</accordion.itemlabel>					<accordion.itemcontent class="panel">{q.answer}</accordion.itemcontent>				</accordion.item>			}		</div>	</accordion.root>}

collapsible={false} keeps one section open: pressing the trigger of the open section does nothing, and only opening the other one closes it.

import { accordion } from '@markless/ui';export default function AccordionLocked() @{	const options = [		{ value: 'ship', name: 'Standard delivery', detail: 'Two working days, no charge over twenty pounds.' },		{ value: 'express', name: 'Express delivery', detail: 'Next working day if you order before noon.' },	];	<accordion.root class="accordion" value="ship" collapsible={false}>		{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}		<div class="sections" role="presentation">			@for (const option of options; key option.value) {				<accordion.item class="section" value={option.value}>					<accordion.itemlabel class="heading">						<accordion.itemtrigger class="trigger">{option.name}</accordion.itemtrigger>					</accordion.itemlabel>					<accordion.itemcontent class="panel">{option.detail}</accordion.itemcontent>				</accordion.item>			}		</div>	</accordion.root>}

Real-world examples

A help centre block. It opens with one answer showing so the section is never a row of empty headings, and multiple lets a reader keep two answers side by side while comparing them.

Frequently asked questions

Still stuck? Write to the help desk and a person answers.

import { accordion } from '@markless/ui';export default function Faq() @{	const questions = [		{			value: 'ship',			question: 'When does my order ship?',			answer:				'Orders placed before 3pm leave the same working day. You get a tracking link as soon as the parcel is scanned.',		},		{			value: 'returns',			question: 'How do I return something?',			answer:				'Start a return from your orders page within 30 days. Post it back with the printed label and the refund lands about a week later.',		},		{			value: 'overseas',			question: 'Do you ship overseas?',			answer:				'Everywhere but Antarctica, which keeps losing our parcels. Duties are shown at checkout, so nothing turns up with a bill attached.',		},		{			value: 'invoice',			question: 'Can I have a VAT invoice?',			answer:				'Every order page has a Download invoice button. Add your company details first and they are printed on it.',		},	];	<section class="faq">		<h2 class="faq-title">Frequently asked questions</h2>		<accordion.root class="faq-list" multiple value={['ship']}>			{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}			<div class="faq-items" role="presentation">				@for (const q of questions; key q.value) {					<accordion.item class="faq-item" value={q.value}>						<accordion.itemlabel class="faq-heading">							<accordion.itemtrigger class="faq-trigger">{q.question}</accordion.itemtrigger>						</accordion.itemlabel>						<accordion.itemcontent class="faq-panel">{q.answer}</accordion.itemcontent>					</accordion.item>				}			</div>		</accordion.root>		<p class="faq-foot">Still stuck? Write to the help desk and a person answers.</p>	</section>}

Three groups of settings, one open at a time. Each trigger carries the group name and a line saying what is inside, and both sit in the one button, so the group is a single stop for the keyboard.

import { accordion } from '@markless/ui';export default function Settings() @{	const groups = [		{			value: 'profile',			name: 'Profile',			note: 'Name and public handle',			tone: 'default',			fields: [				{ id: 'settings-name', name: 'name', label: 'Display name', value: 'Ada Lovelace', placeholder: '' },				{ id: 'settings-handle', name: 'handle', label: 'Handle', value: 'ada-lovelace', placeholder: '' },			],			checks: [],			actions: [],		},		{			value: 'notifications',			name: 'Notifications',			note: 'What we are allowed to email you about',			tone: 'default',			fields: [],			checks: [				{ name: 'digest', label: 'Weekly digest', checked: true },				{ name: 'mentions', label: 'Someone mentions me', checked: false },			],			actions: [],		},		{			value: 'danger',			name: 'Danger zone',			note: 'Deleting an account cannot be undone',			tone: 'danger',			fields: [{ id: 'settings-confirm', name: 'confirm', label: 'Type DELETE to confirm', value: '', placeholder: 'DELETE' }],			checks: [],			actions: [{ name: 'delete', label: 'Delete this account' }],		},	];	<accordion.root class="settings" value="profile">		{/* A repeat cannot open directly inside a component tag, so a host element carries it. */}		<div class="groups" role="presentation">			@for (const group of groups; key group.value) {				<accordion.item class="group" value={group.value} data-tone={group.tone}>					<accordion.itemlabel class="group-heading">						<accordion.itemtrigger class="group-trigger">							<span class="group-name">{group.name}</span>							<span class="group-note">{group.note}</span>						</accordion.itemtrigger>					</accordion.itemlabel>					<accordion.itemcontent class="group-panel">						<div class="group-fields" role="presentation">							@for (const field of group.fields; key field.id) {								<div class="field">									<label class="field-name" for={field.id}>{field.label}</label>									<input class="field-input" id={field.id} name={field.name} value={field.value} placeholder={field.placeholder} />								</div>							}							@for (const check of group.checks; key check.name) {								<label class="check">									<input class="check-box" type="checkbox" name={check.name} checked={check.checked} />									<span>{check.label}</span>								</label>							}							@for (const action of group.actions; key action.name) {								<button class="danger-button" type="button">{action.label}</button>							}						</div>					</accordion.itemcontent>				</accordion.item>			}		</div>	</accordion.root>}

Anatomy

PartWhat it is
accordion.rootThe accordion itself; every section goes inside it. It holds which section is showing, and every other accordion part reads that from here rather than keeping a copy.
accordion.itemOne section: its label, trigger and panel go inside. It carries the section's own value, which is the name the root's value matches against.
accordion.itemlabelThe heading a section's trigger sits in. The APG asks for one, and it is what names the panel: accordion.itemcontent points its aria-labelledby here.
accordion.itemtriggerA consumer's onClick runs after the section has opened or closed.
accordion.itemcontentThe panel. It stays in the page when its section is closed - hidden decides whether it shows, never an arm - so focus, scroll position and the ids other parts point at all survive a close.

Keyboard

KeyWhat it does
Focus the next trigger, wrapping round to the first.
Focus the previous trigger, wrapping round to the last.
HomeHomeFocus the first trigger.
EndEndFocus the last trigger.
EnterorSpaceOpen or close the focused section.

API reference

accordion.root

PropWhat it does
valueWhich section is showing. A plain string names one section; with multiple it is the list of sections that are showing. Omit it and everything starts closed. Default ''.
multipleSeveral sections may show at once. Without it, opening one closes the rest. Default false.
collapsibleWhether the section that is showing may be closed again, leaving nothing open. On by default. Default true.
disabledNothing opens or closes while this is set. Default false.
disableUntilFoundClosed panels are hidden outright rather than with until-found, so the browser's find-in-page cannot reach the text inside them. Default false.
onChangeCalled with the new value when a person opens or closes a section. Omit it and the accordion still works; the call site simply does nothing.

accordion.item

PropWhat it does
valueRequired. This section's own name, and what the root's value names.
disabledThis one section opens and closes for nobody. Default false.

accordion.itemlabel

No props of its own.

accordion.itemtrigger

No props of its own.

accordion.itemcontent

No props of its own.