Accordion
An accordion is a vertically stacked set of interactive headings containing a title, content snippet, or thumbnail representing a section of content.
Features
- Full keyboard navigation.
- Can expand one or multiple items.
- Collapse each accordion item.
Installation
To use the accordion machine in your project, run the following command in your command line:
npm install @zag-js/accordion @zag-js/react # or yarn add @zag-js/accordion @zag-js/react
npm install @zag-js/accordion @zag-js/solid # or yarn add @zag-js/accordion @zag-js/solid
npm install @zag-js/accordion @zag-js/vue # or yarn add @zag-js/accordion @zag-js/vue
npm install @zag-js/accordion @zag-js/svelte # or yarn add @zag-js/accordion @zag-js/svelte
This command will install the framework agnostic accordion logic and the reactive utilities for your framework of choice.
Anatomy
To set up the accordion correctly, you'll need to understand its anatomy and how we name its parts.
Each part includes a
data-part
attribute to help identify them in the DOM.
Usage
First, import the accordion package into your project
import * as accordion from "@zag-js/accordion"
The accordion package exports two key functions:
machine
— The state machine logic for the accordion widget.connect
— The function that translates the machine's state to JSX attributes and event handlers.
You'll also need to provide a unique
id
to theuseMachine
hook. This is used to ensure that every part has a unique identifier.
Next, import the required hooks and functions for your framework and use the accordion machine in your project 🔥
import * as accordion from "@zag-js/accordion" import { useMachine, normalizeProps } from "@zag-js/react" const data = [ { title: "Watercraft", content: "Sample accordion content" }, { title: "Automobiles", content: "Sample accordion content" }, { title: "Aircrafts", content: "Sample accordion content" }, ] function Accordion() { const [state, send] = useMachine(accordion.machine({ id: "1" })) const api = accordion.connect(state, send, normalizeProps) return ( <div {...api.getRootProps()}> {data.map((item) => ( <div {...api.getItemProps({ value: item.title })}> <h3> <button {...api.getItemTriggerProps({ value: item.title })}> {item.title} </button> </h3> <div {...api.getItemContentProps({ value: item.title })}> {item.content} </div> </div> ))} </div> ) }
import * as accordion from "@zag-js/accordion" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, For } from "solid-js" const data = [ { title: "Watercraft", content: "Sample accordion content" }, { title: "Automobiles", content: "Sample accordion content" }, { title: "Aircrafts", content: "Sample accordion content" }, ] function Accordion() { const [state, send] = useMachine(accordion.machine({ id: createUniqueId() })) const api = createMemo(() => accordion.connect(state, send, normalizeProps)) return ( <div {...api().getRootProps()}> <For each={data}> {(item) => ( <div {...api().getItemProps({ value: item.title })}> <h3> <button {...api().getItemTriggerProps({ value: item.title })}> {item.title} </button> </h3> <div {...api().getItemContentProps({ value: item.title })}> {item.content} </div> </div> )} </For> </div> ) }
<script setup> import * as accordion from "@zag-js/accordion" import { normalizeProps, useMachine } from "@zag-js/vue" import { computed } from "vue" const data = [ { title: "Watercraft", content: "Sample accordion content" }, { title: "Automobiles", content: "Sample accordion content" }, { title: "Aircrafts", content: "Sample accordion content" }, ] const [state, send] = useMachine(accordion.machine({ id: "1" })) const api = computed(() => accordion.connect(state.value, send, normalizeProps), ) </script> <template> <div ref="ref" v-bind="api.getRootProps()"> <div v-for="item in data" :key="item.id" v-bind="api.getItemProps({ value: item.title })" > <h3> <button v-bind="api.getItemTriggerProps({ value: item.title })"> {{ item.title }} </button> </h3> <div v-bind="api.getItemContentProps({ value: item.title })"> {{ item.content }} </div> </div> </div> </template>
<script lang="ts"> import * as accordion from "@zag-js/accordion" import { useMachine, normalizeProps } from "@zag-js/svelte" const data = [ { title: "Watercraft", content: "Sample accordion content" }, { title: "Automobiles", content: "Sample accordion content" }, { title: "Aircrafts", content: "Sample accordion content" }, ] const [snapshot, send] = useMachine(accordion.machine({ id: "1" })) const api = $derived(accordion.connect(snapshot, send, normalizeProps)) </script> <div {...api.getRootProps()}> {#each data as item} <div {...api.getItemProps({ value: item.item.title })}> <h3> <button {...api.getItemTriggerProps({ value: item.item.title })}> {item.title} </button> </h3> <div {...api.getItemContentProps({ value: item.item.title })}> {item.content} </div> </div> {/each} </div>
You may have noticed we wrapped each accordion trigger within an h3
. This is
recommended by the
WAI-ARIA
design pattern to ensure the accordion has the appropriate hierarchy on the
page.
Opening multiple accordions at once
To allow multiple items to be expanded at once, set multiple
to true
. This
mode implicitly sets collapsible
to true
and ensures that each accordion can
be expanded.
const [state, send] = useMachine( accordion.machine({ multiple: true, }), )
Opening specific accordions
To set the value of the accordion(s) that should be opened initially, pass the
value
property to the machine function.
// for multiple accordions const [state, send] = useMachine( accordion.machine({ multiple: true, value: ["home"], }), ) // for single accordions const [state, send] = useMachine( accordion.machine({ value: ["home"], }), )
Toggle each accordion item
To collapse an already expanded accordion item by clicking on it, set the
context's collapsible
property to true
.
Note: If
multiple
istrue
, we internally setcollapsible
to betrue
.
const [state, send] = useMachine( accordion.machine({ collapsible: true, }), )
Listening for changes
When the accordion value changes, the onValueChange
callback is invoked.
const [state, send] = useMachine( accordion.machine({ onValueChange(details) { // details => { value: string[] } console.log("selected accordion:", details.value) }, }), )
Disabling an accordion item
To disable a specific accordion item, pass the disabled: true
property to the
getItemProps
, getItemTriggerProps
and getItemContentProps
.
When an accordion item is disabled, it is skipped from keyboard navigation and can't be interacted with.
//... <div {...api.getItemProps({ value: "item", disabled: true })}> <h3> <button {...api.getItemTriggerProps({ value: "item", disabled: true })}> Trigger </button> </h3> <div {...api.getItemContentProps({ value: "item", disabled: true })}> Content </div> </div> //...
You can also disable the entire accordion items by passing disabled
to the
machine's context.
const [state, send] = useMachine( accordion.machine({ disabled: true, }), )
Styling guide
Earlier, we mentioned that each accordion part has a data-part
attribute added
to them to select and style them in the DOM.
Open and closed state
When an accordion item is expanded or collapsed, a data-state
attribute is set
on the item, trigger and content elements. This attribute is removed when it is
closed.
[data-part="item"][data-state="open|closed"] { /* styles for the item is open or closed state */ } [data-part="item-trigger"][data-state="open|closed"] { /* styles for the item is open or closed state */ } [data-part="item-content"][data-state="open|closed"] { /* styles for the item is open or closed state */ }
Focused state
When an accordion item's trigger is focused, a data-focus
attribute is set on
the item and content.
[data-part="item"][data-focus] { /* styles for the item's focus state */ } [data-part="item-trigger"]:focus { /* styles for the trigger's focus state */ } [data-part="item-content"][data-focus] { /* styles for the content's focus state */ }
Creating Component
Create your accordion component by abstracting the machine into your own component.
Usage
import { Accordion } from "./your-accordion" function Demo() { return ( <Accordion defaultValue={["1"]} items={[ { value: "1", title: "Title 1", content: "Content 1" }, { value: "2", title: "Title 2", content: "Content 2" }, ]} /> ) }
import { Accordion } from "./your-accordion" function Demo() { return ( <Accordion value={["1"]} items={[ { value: "1", title: "Title 1", content: "Content 1" }, { value: "2", title: "Title 2", content: "Content 2" }, ]} /> ) }
Implementation
Use the the splitProps
utility to separate the machine's props from the
component's props.
import * as accordion from "@zag-js/accordion" import { useMachine, normalizeProps } from "@zag-js/react" interface Item { value: string title: React.ReactNode content: React.ReactNode } export interface AccordionProps extends Omit<accordion.Context, "id"> { defaultValue?: accordion.Context["value"] items: Item[] } export function Accordion(props: AccordionProps) { const [machineProps, localProps] = accordion.splitProps(props) const [state, send] = useMachine( accordion.machine({ id: useId(), value: defaultValue }), { context: machineProps }, ) const api = accordion.connect(state, send, normalizeProps) return ( <div {...api.getRootProps()}> {localProps.items.map((item) => ( <div {...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> ) }
import * as accordion from "@zag-js/accordion" import { normalizeProps, useMachine } from "@zag-js/solid" import { createMemo, createUniqueId, For, JSX } from "solid-js" interface Item { value: string title: JSX.Element content: JSX.Element } export interface AccordionProps extends Omit<accordion.Context, "id"> { items: Item[] } export function Accordion(props: AccordionProps) { const [machineProps, localProps] = splitProps(props, accordion.props) const [state, send] = useMachine( accordion.machine({ id: createUniqueId() }), { context: machineProps }, ) const api = createMemo(() => accordion.connect(state, send, normalizeProps)) return ( <div {...api().getRootProps()}> <For each={localProps.items}> {(item) => ( <div {...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> )} </For> </div> ) }
Methods and Properties
The accordion's api
exposes the following methods and properties:
Machine Context
The accordion machine exposes the following context properties:
ids
Partial<{ root: string; item(value: string): string; itemContent(value: string): string; itemTrigger(value: string): string; }>
The ids of the elements in the accordion. Useful for composition.multiple
boolean
Whether multple accordion items can be expanded at the same time.collapsible
boolean
Whether an accordion item can be closed after it has been expanded.value
string[]
The `value` of the accordion items that are currently being expanded.disabled
boolean
Whether the accordion items are disabledonValueChange
(details: ValueChangeDetails) => void
The callback fired when the state of expanded/collapsed accordion items changes.onFocusChange
(details: FocusChangeDetails) => void
The callback fired when the focused accordion item changes.orientation
"horizontal" | "vertical"
The orientation of the accordion items.dir
"ltr" | "rtl"
The document's text/writing direction.id
string
The unique identifier of the machine.getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The accordion api
exposes the following methods:
focusedValue
string
The value of the focused accordion item.value
string[]
The value of the accordionsetValue
(value: string[]) => void
Sets the value of the accordion.getItemState
(props: ItemProps) => ItemState
Gets the state of an accordion item.
Data Attributes
Accessibility
Keyboard Interactions
- SpaceWhen focus is on an trigger of a collapsed item, the item is expanded
- EnterWhen focus is on an trigger of a collapsed section, expands the section.
- TabMoves focus to the next focusable element
- Shift + TabMoves focus to the previous focusable element
- ArrowDownMoves focus to the next trigger
- ArrowUpMoves focus to the previous trigger.
- HomeWhen focus is on an trigger, moves focus to the first trigger.
- EndWhen focus is on an trigger, moves focus to the last trigger.
Edit this page on GitHub