Build a headless, accessible accordion whose pieces share state through React context. Callers should be able to compose Accordion.Item, Accordion.Trigger, and Accordion.Panel freely while the root enforces one-open-at-a-time behavior and each item maintains correct accessibility links.
function Accordion(props: {
defaultValue?: unknown;
children?: React.ReactNode;
}): React.ReactNode;
Accordion.Item(props: {
value: unknown;
children?: React.ReactNode;
}): React.ReactNode;
Accordion.Trigger(
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
): React.ReactElement;
Accordion.Panel(
props: React.HTMLAttributes<HTMLDivElement>,
): React.ReactElement;
Export { Accordion } from compoundComponent.js. Attach the three leaf components as static properties: Accordion.Item, Accordion.Trigger, and Accordion.Panel.
const accordion = React.createElement(
Accordion,
{ defaultValue: 'billing' },
React.createElement(
Accordion.Item,
{ value: 'billing' },
React.createElement(Accordion.Trigger, null, 'Billing'),
React.createElement(Accordion.Panel, null, 'Update payment details'),
),
React.createElement(
Accordion.Item,
{ value: 'shipping' },
React.createElement(Accordion.Trigger, null, 'Shipping'),
React.createElement(Accordion.Panel, null, 'Change delivery address'),
),
);
// Billing begins open. Clicking Shipping closes Billing and opens Shipping.
// Clicking Shipping again closes it, leaving both panels hidden.
A caller can cancel the built-in transition:
React.createElement(
Accordion.Trigger,
{
className: 'dangerous-action',
onClick(event) {
if (!window.confirm('Reveal this section?')) event.preventDefault();
},
},
React.createElement('strong', null, 'Private details'),
);
defaultValue, which defaults to null. This is uncontrolled state: later defaultValue changes do not replace live state.null.Accordion.Item requires a value prop and supplies its value plus accessibility metadata to its descendants. It adds no DOM wrapper.button, default its type to "button", and support normal button props and arbitrary children. Call a supplied onClick first. Toggle only when that event has not been default-prevented.div with role="region"; use the hidden attribute when its item is closed. Support normal div props and arbitrary children.aria-controls must name its panel, and the panel's aria-labelledby must name its trigger. IDs must be unique and stable across state changes and parent rerenders. The trigger also exposes the current boolean through aria-expanded.value, or when a trigger or panel is outside an item.Do not add controlled state, roving keyboard focus, animation, persistence, multi-select behavior, or CSS. The browser's native button keyboard behavior is sufficient for this task.
The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
Build a headless, accessible accordion whose pieces share state through React context. Callers should be able to compose Accordion.Item, Accordion.Trigger, and Accordion.Panel freely while the root enforces one-open-at-a-time behavior and each item maintains correct accessibility links.
function Accordion(props: {
defaultValue?: unknown;
children?: React.ReactNode;
}): React.ReactNode;
Accordion.Item(props: {
value: unknown;
children?: React.ReactNode;
}): React.ReactNode;
Accordion.Trigger(
props: React.ButtonHTMLAttributes<HTMLButtonElement>,
): React.ReactElement;
Accordion.Panel(
props: React.HTMLAttributes<HTMLDivElement>,
): React.ReactElement;
Export { Accordion } from compoundComponent.js. Attach the three leaf components as static properties: Accordion.Item, Accordion.Trigger, and Accordion.Panel.
const accordion = React.createElement(
Accordion,
{ defaultValue: 'billing' },
React.createElement(
Accordion.Item,
{ value: 'billing' },
React.createElement(Accordion.Trigger, null, 'Billing'),
React.createElement(Accordion.Panel, null, 'Update payment details'),
),
React.createElement(
Accordion.Item,
{ value: 'shipping' },
React.createElement(Accordion.Trigger, null, 'Shipping'),
React.createElement(Accordion.Panel, null, 'Change delivery address'),
),
);
// Billing begins open. Clicking Shipping closes Billing and opens Shipping.
// Clicking Shipping again closes it, leaving both panels hidden.
A caller can cancel the built-in transition:
React.createElement(
Accordion.Trigger,
{
className: 'dangerous-action',
onClick(event) {
if (!window.confirm('Reveal this section?')) event.preventDefault();
},
},
React.createElement('strong', null, 'Private details'),
);
defaultValue, which defaults to null. This is uncontrolled state: later defaultValue changes do not replace live state.null.Accordion.Item requires a value prop and supplies its value plus accessibility metadata to its descendants. It adds no DOM wrapper.button, default its type to "button", and support normal button props and arbitrary children. Call a supplied onClick first. Toggle only when that event has not been default-prevented.div with role="region"; use the hidden attribute when its item is closed. Support normal div props and arbitrary children.aria-controls must name its panel, and the panel's aria-labelledby must name its trigger. IDs must be unique and stable across state changes and parent rerenders. The trigger also exposes the current boolean through aria-expanded.value, or when a trigger or panel is outside an item.Do not add controlled state, roving keyboard focus, animation, persistence, multi-select behavior, or CSS. The browser's native button keyboard behavior is sufficient for this task.
The full solution is part of Premium
Walkthrough, edge cases, complexity notes, and the runnable editor unlock with a Premium subscription.
Submissions are part of Premium
Unlock community code, comments, reactions, and framework-specific approaches.
Unlock the solution & editor
Runnable editor + tests
Solve in the browser with instant Jest feedback.
Detailed solutions
Walkthroughs, edge cases, and complexity notes.
Multi-framework variants
React, Vue, Vanilla, Angular — same question, different stacks.