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; }> | undefinedThe ids of the elements in the accordion. Useful for composition.multipleboolean | undefinedWhether multiple accordion items can be expanded at the same time.collapsibleboolean | undefinedWhether an accordion item can be closed after it has been expanded.valuestring[] | undefinedThe controlled value of the expanded accordion items.defaultValuestring[] | undefinedThe initial value of the expanded accordion items. Use when you don't need to control the value of the accordion.disabledboolean | undefinedWhether the accordion items are disabledonValueChange((details: ValueChangeDetails) => void) | undefinedThe callback fired when the state of expanded/collapsed accordion items changes.onFocusChange((details: FocusChangeDetails) => void) | undefinedThe callback fired when the focused accordion item changes.orientation"horizontal" | "vertical" | undefinedThe orientation of the accordion items.dir"ltr" | "rtl" | undefinedThe document's text/writing direction.idstringThe unique identifier of the machine.getRootNode(() => ShadowRoot | Node | Document) | undefinedA root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
Machine API
The accordion api exposes the following methods:
focusedValuestring | nullThe 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.