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
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-partattribute 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
idto theuseMachinehook. 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>
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
multipleistrue, we internally setcollapsibleto 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:
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 multple accordion items can be expanded at the same time.collapsiblebooleanWhether an accordion item can be closed after it has been expanded.valuestring[]The `value` of the accordion items that are currently being expanded.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 accordion.getItemState(props: ItemProps) => ItemStateGets 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