Accordion
Accordion coordinates collapsible AccordionItem sections. It owns its state by default and supports caller-owned state.
Open in Storybook
Import
import { Accordion, AccordionItem } from "@tosui/react";
Basic usage
- Preview
- Code
<Accordion>
<AccordionItem title="Section 1">
Content for section 1
</AccordionItem>
<AccordionItem title="Section 2">
Content for section 2
</AccordionItem>
<AccordionItem title="Section 3">
Content for section 3
</AccordionItem>
</Accordion>
Default expanded item
- Preview
- Code
This section is open by default.
<Accordion defaultIndex={0}>
<AccordionItem title="Initially Open">
This section is open by default.
</AccordionItem>
<AccordionItem title="Closed">
This section starts closed.
</AccordionItem>
</Accordion>
Multiple expanded items
Pass allowMultiple to use array state and allow more than one expanded item.
- Preview
- Code
This can stay open
While this is also open
<Accordion allowMultiple defaultIndex={[0, 1]}>
<AccordionItem index={0} title="Section A">
This can stay open
</AccordionItem>
<AccordionItem index={1} title="Section B">
While this is also open
</AccordionItem>
<AccordionItem index={2} title="Section C">
Open all three at once!
</AccordionItem>
</Accordion>
Change events
In single mode, onChange receives the expanded item index or null. With allowMultiple, onChange receives all expanded indexes.
<Accordion onChange={(index) => console.log(index)}>
<AccordionItem title="Account">Account settings</AccordionItem>
<AccordionItem title="Security">Security settings</AccordionItem>
</Accordion>
Controlled state
Pass index when application state owns the expanded state. A controlled Accordion reports a requested change through onChange, but it does not update until you pass the new value back.
function ControlledAccordion() {
const [expandedIndex, setExpandedIndex] = useState<number | null>(0);
return (
<Accordion index={expandedIndex} onChange={setExpandedIndex}>
<AccordionItem title="Account">Account settings</AccordionItem>
<AccordionItem title="Security">Security settings</AccordionItem>
</Accordion>
);
}
For multiple expanded items, pass an array together with allowMultiple:
const [expandedIndexes, setExpandedIndexes] = useState<number[]>([0]);
<Accordion
allowMultiple
index={expandedIndexes}
onChange={setExpandedIndexes}
>
{/* AccordionItem children */}
</Accordion>
Disabled items
- Preview
- Code
<Accordion>
<AccordionItem index={0} title="Available">
This section can be toggled.
</AccordionItem>
<AccordionItem index={1} title="Disabled Section" disabled>
This section cannot be opened.
</AccordionItem>
<AccordionItem index={2} title="Also Available">
This section can be toggled too.
</AccordionItem>
</Accordion>
Item indexes
Accordion assigns an index from each direct child's position. Pass index to override that value. An AccordionItem that is not a direct child and does not have an explicit index throws an error.
<Accordion>
<AccordionItem title="Automatic index 0">First</AccordionItem>
<AccordionItem index={10} title="Explicit index 10">Second</AccordionItem>
</Accordion>
Common patterns
FAQ section
<Accordion>
<AccordionItem index={0} title="What is Tosui?">
Tosui is a themable, orderly, simple UI component library built on
constraint-driven simplicity.
</AccordionItem>
<AccordionItem index={1} title="How do I install it?">
<Text>Run the following command:</Text>
<Code variant="subtle" mt={2} display="block">npm install @tosui/react</Code>
</AccordionItem>
<AccordionItem index={2} title="Is it accessible?">
Accordion headers are native buttons. They expose expanded state and control
their associated panels.
</AccordionItem>
</Accordion>
Settings sections
<Accordion allowMultiple>
<AccordionItem index={0} title="Account Settings">
<VStack gap={3} align="stretch">
<HStack justify="space-between">
<Text>Email notifications</Text>
<Switch />
</HStack>
<HStack justify="space-between">
<Text>Two-factor authentication</Text>
<Switch />
</HStack>
</VStack>
</AccordionItem>
<AccordionItem index={1} title="Privacy Settings">
<VStack gap={3} align="stretch">
<HStack justify="space-between">
<Text>Profile visibility</Text>
<Select defaultValue="public">
<option value="public">Public</option>
<option value="private">Private</option>
</Select>
</HStack>
</VStack>
</AccordionItem>
</Accordion>
Product features
<Accordion>
<AccordionItem index={0} title="Free Plan">
<List styleType="disc" spacing={2}>
<ListItem>5 projects</ListItem>
<ListItem>Basic support</ListItem>
<ListItem>1GB storage</ListItem>
</List>
</AccordionItem>
<AccordionItem index={1} title="Pro Plan">
<List styleType="disc" spacing={2}>
<ListItem>Unlimited projects</ListItem>
<ListItem>Priority support</ListItem>
<ListItem>100GB storage</ListItem>
<ListItem>Custom domains</ListItem>
</List>
</AccordionItem>
</Accordion>
Props reference
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
| index | number | null | - | Controlled expanded index in single mode |
| defaultIndex | number | - | Initial expanded index in single mode |
| allowMultiple | false | false | Select single mode |
| onChange | (index: number | null) => void | - | Receive the requested single-mode state |
| className | string | - | Additional CSS class |
| children | ReactNode | - | AccordionItem children |
With allowMultiple={true}, the state props use arrays:
| Prop | Type | Default | Description |
|---|---|---|---|
| index | number[] | - | Controlled expanded indexes |
| defaultIndex | number[] | [] | Initial expanded indexes |
| allowMultiple | true | - | Select multiple mode |
| onChange | (index: number[]) => void | - | Receive the requested multiple-mode state |
AccordionItem
| Prop | Type | Default | Description |
|---|---|---|---|
| index | number | Item position | Optional item index override |
| title | ReactNode | - | Item header (required) |
| disabled | boolean | false | Whether item is disabled |
| className | string | - | Additional CSS class |
| children | ReactNode | - | Item content |
Accessibility
- Each trigger button uses
aria-expandedandaria-controls. - Each panel uses
aria-labelledbyandrole="region". - A collapsed panel stays mounted to preserve its local state and transition.
- A collapsed panel is both
aria-hiddenand inert, so its descendants cannot receive focus. - A disabled trigger uses
aria-disabledand ignores activation. - Native button behavior supports Tab, Enter, and Space.
TypeScript
import { Accordion, AccordionItem, type AccordionProps, type AccordionItemProps } from "@tosui/react";