Skip to main content
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>
  )
}
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

multiple
boolean
default:"false"
Whether multiple accordion items can be expanded at the same time.
collapsible
boolean
default:"false"
Whether an accordion item can be closed after it has been expanded.
value
string[]
The controlled value of the expanded accordion items.
defaultValue
string[]
The initial value of the expanded accordion items. Use when you don’t need to control the value.
disabled
boolean
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

PartElementDescription
rootdivThe root container
itemdivAn individual accordion item
item-triggerbuttonThe button that toggles an item
item-contentdivThe content panel of an item
item-indicatordivOptional 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

KeyDescription
Enter / SpaceToggles the focused accordion item.
TabMoves focus to the next focusable element.
Shift + TabMoves focus to the previous focusable element.
ArrowDownMoves focus to the next accordion trigger (vertical orientation).
ArrowUpMoves focus to the previous accordion trigger (vertical orientation).
ArrowRightMoves focus to the next accordion trigger (horizontal orientation).
ArrowLeftMoves focus to the previous accordion trigger (horizontal orientation).
HomeMoves focus to the first accordion trigger.
EndMoves focus to the last accordion trigger.

Build docs developers (and LLMs) love