An accordion is a vertically stacked set of interactive headings containing a title, content snippet, or thumbnail representing a section of content. It follows the WAI-ARIA Accordion pattern and provides full keyboard navigation.
Features
- Full keyboard navigation
- Supports single and multiple expanded items
- Supports collapsible items
- Supports horizontal and vertical orientation
- Controlled and uncontrolled modes
Installation
npm install @zag-js/accordion @zag-js/react
Usage
Import the accordion package and connect the machine to your framework:
import * as accordion from "@zag-js/accordion"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId } from "react"
The accordion package exports two key functions:
machine — State machine logic.
connect — Maps machine state to JSX props and event handlers.
import * as accordion from "@zag-js/accordion"
import { useMachine, normalizeProps } from "@zag-js/react"
import { useId } from "react"
const items = [
{ value: "item-1", title: "What is Zag?", content: "Zag is a framework-agnostic UI component library built on state machines." },
{ value: "item-2", title: "How does it work?", content: "Each component is powered by a finite state machine with built-in accessibility." },
{ value: "item-3", title: "Which frameworks are supported?", content: "React, Vue, Solid, Svelte, and Preact." },
]
export function Accordion() {
const service = useMachine(accordion.machine, {
id: useId(),
collapsible: true,
})
const api = accordion.connect(service, normalizeProps)
return (
<div {...api.getRootProps()}>
{items.map((item) => (
<div key={item.value} {...api.getItemProps({ value: item.value })}>
<h3>
<button {...api.getItemTriggerProps({ value: item.value })}>
{item.title}
</button>
</h3>
<div {...api.getItemContentProps({ value: item.value })}>
{item.content}
</div>
</div>
))}
</div>
)
}
<script setup>
import * as accordion from "@zag-js/accordion"
import { useMachine, normalizeProps } from "@zag-js/vue"
import { useId, computed } from "vue"
const items = [
{ value: "item-1", title: "What is Zag?", content: "Zag is a framework-agnostic UI component library built on state machines." },
{ value: "item-2", title: "How does it work?", content: "Each component is powered by a finite state machine with built-in accessibility." },
]
const service = useMachine(accordion.machine, {
id: useId(),
collapsible: true,
})
const api = computed(() => accordion.connect(service, normalizeProps))
</script>
<template>
<div v-bind="api.getRootProps()">
<div
v-for="item in items"
:key="item.value"
v-bind="api.getItemProps({ value: item.value })"
>
<h3>
<button v-bind="api.getItemTriggerProps({ value: item.value })">
{{ item.title }}
</button>
</h3>
<div v-bind="api.getItemContentProps({ value: item.value })">
{{ item.content }}
</div>
</div>
</div>
</template>
Wrap each accordion trigger in an h3 (or appropriate heading level). This is recommended by the WAI-ARIA design pattern to ensure the accordion has the correct document hierarchy.
Opening multiple items
Set multiple to true to allow more than one expanded item at a time.
const service = useMachine(accordion.machine, {
multiple: true,
})
Setting the initial value
Set defaultValue to define which items are expanded on first render.
// Multiple mode
const service = useMachine(accordion.machine, {
multiple: true,
defaultValue: ["item-1", "item-2"],
})
// Single mode
const service = useMachine(accordion.machine, {
defaultValue: ["item-1"],
})
Controlled accordion
Use value and onValueChange to control expanded items externally.
const [value, setValue] = useState(["item-1"])
const service = useMachine(accordion.machine, {
value,
onValueChange(details) {
// details => { value: string[] }
setValue(details.value)
},
})
Making items collapsible
Set collapsible to true to allow closing an expanded item by clicking it again.
const service = useMachine(accordion.machine, {
collapsible: true,
})
When multiple is true, collapsible is internally set to true automatically.
Listening for focus changes
Use onFocusChange to react when keyboard focus moves between item triggers.
const service = useMachine(accordion.machine, {
onFocusChange(details) {
// details => { value: string | null }
console.log("focused item:", details.value)
},
})
Horizontal orientation
Set orientation to "horizontal" when rendering items side by side.
const service = useMachine(accordion.machine, {
orientation: "horizontal",
})
Disabling an accordion item
Pass disabled: true to item props to disable a specific item. Disabled items are skipped from keyboard navigation.
<div {...api.getItemProps({ value: "item-1", disabled: true })}>
<h3>
<button {...api.getItemTriggerProps({ value: "item-1", disabled: true })}>
Trigger
</button>
</h3>
<div {...api.getItemContentProps({ value: "item-1", disabled: true })}>
Content
</div>
</div>
You can also disable the entire accordion:
const service = useMachine(accordion.machine, {
disabled: true,
})
API Reference
Whether multiple accordion items can be expanded at the same time.
Whether an accordion item can be closed after it has been expanded.
The controlled value of the expanded accordion items.
The initial value of the expanded accordion items. Use when you don’t need to control the value.
Whether all accordion items are disabled.
orientation
"horizontal" | "vertical"
default:"\"vertical\""
The orientation of the accordion items.
onValueChange
(details: { value: string[] }) => void
Callback fired when the set of expanded items changes.
onFocusChange
(details: { value: string | null }) => void
Callback fired when the focused accordion item changes.
Styling
Each part includes a data-part attribute you can target in CSS.
Parts
| Part | Element | Description |
|---|
root | div | The root container |
item | div | An individual accordion item |
item-trigger | button | The button that toggles an item |
item-content | div | The content panel of an item |
item-indicator | div | Optional visual indicator (e.g. chevron) |
Open and closed state
When an accordion item expands or collapses, data-state is set to "open" or "closed" on the item, trigger, and content elements.
[data-part="item"][data-state="open"] {
/* styles for the open state */
}
[data-part="item"][data-state="closed"] {
/* styles for the closed state */
}
[data-part="item-trigger"][data-state="open"] {
/* styles for the trigger open state */
}
[data-part="item-content"][data-state="open"] {
/* styles for the content open state */
}
Focused state
When an accordion trigger is focused, data-focus is set on the item and content.
[data-part="item"][data-focus] {
/* styles for the focused item */
}
[data-part="item-trigger"]:focus {
/* styles for the focused trigger */
}
[data-part="item-content"][data-focus] {
/* styles for the focused content */
}
Disabled state
When an accordion item is disabled, data-disabled is set on the item, trigger, and content.
[data-part="item"][data-disabled] {
/* styles for the disabled item */
}
[data-part="item-trigger"][data-disabled] {
/* styles for the disabled trigger */
}
Accessibility
The accordion follows the WAI-ARIA Accordion pattern.
- Each trigger has
role="button" and aria-expanded reflecting the item state.
- Trigger elements are associated with their content panels via
aria-controls.
- Content panels use
role="region" and aria-labelledby.
Keyboard interactions
| Key | Description |
|---|
Enter / Space | Toggles the focused accordion item. |
Tab | Moves focus to the next focusable element. |
Shift + Tab | Moves focus to the previous focusable element. |
ArrowDown | Moves focus to the next accordion trigger (vertical orientation). |
ArrowUp | Moves focus to the previous accordion trigger (vertical orientation). |
ArrowRight | Moves focus to the next accordion trigger (horizontal orientation). |
ArrowLeft | Moves focus to the previous accordion trigger (horizontal orientation). |
Home | Moves focus to the first accordion trigger. |
End | Moves focus to the last accordion trigger. |