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
- Supports single and multiple expanded items
- Supports collapsible items
- Supports horizontal and vertical orientation
Installation
Install the accordion package:
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
Anatomy
Check the accordion anatomy and part names.
Each part includes a
data-partattribute to help identify them in the DOM.
Usage
Import the accordion package:
import * as accordion from "@zag-js/accordion"
The accordion package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Pass a unique
idtouseMachineso generated element ids stay predictable.
Then use the framework integration helpers:
import * as accordion from "@zag-js/accordion" import { useMachine, normalizeProps } from "@zag-js/react" import { useId } from "react" const data = [ { title: "Watercraft", content: "Sample accordion content" }, { title: "Automobiles", content: "Sample accordion content" }, { title: "Aircraft", content: "Sample accordion content" }, ] function Accordion() { const service = useMachine(accordion.machine, { id: useId() }) const api = accordion.connect(service, normalizeProps) return ( <div {...api.getRootProps()}> {data.map((item) => ( <div key={item.title} {...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: "Aircraft", content: "Sample accordion content" }, ] function Accordion() { const service = useMachine(accordion.machine, { id: createUniqueId() }) const api = createMemo(() => accordion.connect(service, 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: "Aircraft", content: "Sample accordion content" }, ] const service = useMachine(accordion.machine, { id: "1" }) const api = computed(() => accordion.connect(service, 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: "Aircraft", content: "Sample accordion content" }, ] const id = $props.id() const service = useMachine(accordion.machine, ({ id })) const api = $derived(accordion.connect(service, normalizeProps)) </script> <div {...api.getRootProps()}> {#each data as 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> {/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 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 expanded items on first render.
// multiple mode const service = useMachine(accordion.machine, { multiple: true, defaultValue: ["home", "about"], }) // single mode const service = useMachine(accordion.machine, { defaultValue: ["home"], })
Controlled accordions
Use value and onValueChange to control expanded items externally.
import { useState } from "react" export function ControlledAccordion() { const [value, setValue] = useState(["home"]) const service = useMachine(accordion.machine, { value, onValueChange(details) { setValue(details.value) }, }) return ( // ... ) }
import { createSignal } from "solid-js" export function ControlledAccordion() { const [value, setValue] = createSignal(["home"]) const service = useMachine(accordion.machine, { get value() { return value() }, onValueChange(details) { setValue(details.value) }, }) return ( // ... ) }
<script setup> import { ref } from "vue" const valueRef = ref(["home"]) const service = useMachine(accordion.machine, { get value() { return valueRef.value }, onValueChange(details) { valueRef.value = details.value }, }) </script>
<script lang="ts"> let value = $state(["home"]) const service = useMachine(accordion.machine, { get value() { return value }, onValueChange(details) { value = details.value }, }) </script>
Making items collapsible
Set collapsible to true to allow closing an expanded item by clicking it
again.
Note: If
multipleistrue, we internally setcollapsibleto betrue.
const service = useMachine(accordion.machine, { collapsible: true, })
Listening for value changes
When the accordion value changes, the onValueChange callback is invoked.
const service = useMachine(accordion.machine, { onValueChange(details) { // details => { value: string[] } console.log("selected accordion:", details.value) }, })
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
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 by setting disabled on the machine.
const service = useMachine(accordion.machine, { disabled: true, })
Styling guide
Each part includes a data-part attribute you can target in CSS.
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|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 a 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 defaultValue={["1"]} items={[ { value: "1", title: "Title 1", content: "Content 1" }, { value: "2", title: "Title 2", content: "Content 2" }, ]} /> ) }
Implementation
Use 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" import { useId } from "react" interface Item { value: string title: React.ReactNode content: React.ReactNode } export interface AccordionProps extends Omit<accordion.Props, "id"> { items: Item[] } export function Accordion(props: AccordionProps) { const [machineProps, localProps] = accordion.splitProps(props) const service = useMachine(accordion.machine, { id: useId(), ...machineProps, }) const api = accordion.connect(service, 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 { mergeProps, splitProps, 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 context = mergeProps(machineProps, { id: createUniqueId() }) const service = useMachine(accordion.machine, context) const api = createMemo(() => accordion.connect(service, 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:
idsPartial<{ 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.multiplebooleanWhether multiple accordion items can be expanded at the same time.collapsiblebooleanWhether an accordion item can be closed after it has been expanded.valuestring[]The controlled value of the expanded accordion items.defaultValuestring[]The initial value of the expanded accordion items. Use when you don't need to control the value of the accordion.disabledbooleanWhether the accordion items are disabledonValueChange(details: ValueChangeDetails) => voidThe callback fired when the state of expanded/collapsed accordion items changes.onFocusChange(details: FocusChangeDetails) => voidThe 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.idstringThe unique identifier of the machine.getRootNode() => ShadowRoot | Node | DocumentA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The accordion api exposes the following methods:
focusedValuestringThe value of the focused accordion item.valuestring[]The value of the accordionsetValue(value: string[]) => voidSets the value of the accordiongetItemState(props: ItemProps) => ItemStateReturns 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.