Accordion

From the ARIA Authoring Practices Guide (APG):

An accordion is a vertically stacked set of interactive headings that each contain a title, content snippet, or thumbnail representing a section of content. The headings function as controls that enable users to reveal or hide their associated sections of content. Accordions are commonly used to reduce the need to scroll when presenting multiple sections of content on a single page.

Usage and Examples

Components and hooks for the accordion pattern can be imported directly from the package itself, or from the /accordion sub-module.

import { Accordion } from 'react-aria-widgets';
import { Accordion } from 'react-aria-widgets/accordion';

Importing from the sub-module means potentially smaller bundle sizes, but if you're writing a TypeScript application, you may need to change moduleResolution to node16 in your tsconfig.json. For more information, see the FAQ.

Additionally, because these components come with no styling, they will not have the proper expand/collapse behavior out of the box. For the sake of demonstrating the proper behavior, the examples on this page will be given the following styles:

/*
 * .react-aria-widgets-accordion-panel is a CSS class provided by default,
 * and React ARIA Widgets exposes the accordion's state via HTML attributes
 * so they can be targeted with CSS.
 */
.react-aria-widgets-accordion-panel[data-expanded=false] {
  display: none;
}

For more information, see the styling section.

Basic Usage

A basic accordion consists of an <Accordion> wrapping around one or more <AccordionItem>s, where each <AccordionItem> has an <AccordionHeader> and an <AccordionPanel>.

Out of the box, React ARIA Widgets provides the focus control as described in the APG. You can use ArrowDown, ArrowUp, Home, and End to switch focus between each item.

A headerLevel prop must be supplied to the <Accordion>, and each <AccordionItem> must have a unique (amongst its siblings) id prop.

Hello world!

Hello world!

Hello world!

import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

function BasicAccordion() {
  return (
    <Accordion headerLevel={ 4 }>
      <AccordionItem id="item1">
        <AccordionHeader>
          Accordion Item 1
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item2">
        <AccordionHeader>
          Accordion Item 2
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item3">
        <AccordionHeader>
          Accordion Item 3
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
    </Accordion>
  );
}

Disable Multiple Expanded Sections

By default, multiple sections can be expanded and collapsed at the same time, but this behavior can be turned off with the allowMultiple prop.

Hello world!

Hello world!

Hello world!

import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

function DisableMultipleAccordion() {
  return (
    <Accordion headerLevel={ 4 } allowMultiple={ false }>
      <AccordionItem id="item1">
        <AccordionHeader>
          Accordion Item 1
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item2">
        <AccordionHeader>
          Accordion Item 2
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item3">
        <AccordionHeader>
          Accordion Item 3
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
    </Accordion>
  );
}

Disable Collapsing All Sections

By default, all of the accordion items can be simultaneously collapsed. If the allowCollapseLast prop is false, the final expanded section cannot be collapsed.

Hello world!

Hello world!

Hello world!

import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

function DisableCollapseLastAccordion() {
  return (
    <Accordion headerLevel={ 4 } allowCollapseLast={ false }>
      <AccordionItem id="item1">
        <AccordionHeader>
          Accordion Item 1
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item2">
        <AccordionHeader>
          Accordion Item 2
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item3">
        <AccordionHeader>
          Accordion Item 3
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
    </Accordion>
  );
}

Disabling allowMultiple and allowCollapseLast

Hello world!

Hello world!

Hello world!

import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

function DisableBothAccordion() {
  return (
    <Accordion headerLevel={ 4 } allowMultiple={ false } allowCollapseLast={ false }>
      <AccordionItem id="item1">
        <AccordionHeader>
          Accordion Item 1
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item2">
        <AccordionHeader>
          Accordion Item 2
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item3">
        <AccordionHeader>
          Accordion Item 3
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
    </Accordion>
  );
}

Rendering with Render Props

In addition to normal React nodes, <AccordionHeader> and <AccordionPanel> both accept a render function as children. These render functions have access to all of the fields and methods that pertain to the accordion.

  • id = item1
  • headerLevel = 4
  • allowMultiple = true
  • allowCollapseLast = true

  • id = item2
  • headerLevel = 4
  • allowMultiple = true
  • allowCollapseLast = true

  • id = item3
  • headerLevel = 4
  • allowMultiple = true
  • allowCollapseLast = true
import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

const ITEMS = [ 'item1', 'item2', 'item3' ];

function RenderPropAccordion() {
  return (
    <Accordion headerlevel={ 4 }>
      { ITEMS.map((id, index) => (
        <AccordionItem key={ id } id={ id }>
          <AccordionHeader>
            { ({ id, getIsExpanded }) => (
              <>
                Accordion Item { index + 1 }: Expanded = <code>{ getIsExpanded(id).toString() }</code>
              </>
            ) }
          </AccordionHeader>
          <AccordionPanel>
            { ({ id, headerLevel, allowMultiple, allowCollapseLast }) => (
              <ul className="mb-4">
                <li><code>id</code> = <code>{ id }</code></li>
                <li><code>headerLevel</code> = <code>{ headerLevel }</code></li>
                <li><code>allowMultiple</code> = <code>{ allowMultiple.toString() }</code></li>
                <li><code>allowCollapseLast</code> = <code>{ allowCollapseLast.toString() }</code></li>
              </ul>
            ) }
          </AccordionPanel>
        </AccordionItem>
      )) }
    </Accordion>
  );
}

Prevent Expanding/Collapsing Accordion Items

Accordion items can be manually disabled with the method toggleDisabled, preventing them from being expanded/collapsed. Note that <AccordionHeader> will set the aria-disabled attribute, but not the disabled attribute, which has various implications. For example, though the panel will not expand or collapse, the button is still focusable and will still trigger events.

See the MDN Web Docs for more information.

import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

const ITEMS = [ 'item1', 'item2', 'item3' ];

function DisableItemAccordion() {
  return (
    <Accordion headerLevel={ 4 }>
      { ITEMS.map((id, index) => (
        <AccordionItem key={ id } id={ id }>
          <AccordionHeader>
            { ({ id, getIsDisabled }) => (
              <>
                Accordion Item { index + 1 }: Disabled = <code>{ getIsDisabled(id).toString() }</code>
              </>
            ) }
          </AccordionHeader>
          <AccordionPanel>
            { ({ id, toggleDisabled, getIsDisabled }) => (
              <button className="button is-primary mb-4" type="button" onClick={ () => toggleDisabled(id) }>
                { getIsDisabled(id) ? 'Enable' : 'Disable' } Item
              </button>
            ) }
          </AccordionPanel>
        </AccordionItem>
      )) }
    </Accordion>
  );
}

Initialize Expanded/Disabled State

By default, all accordion items are collapsed and enabled. You can initialize certain items to be expanded or disabled by passing arrays of string IDs to the initialExpanded and initialDisabled props.

If allowMultiple is disabled, React ARIA Widgets will only expand the first ID in initialExpanded. However, it does so naively - it essentially just checks initialExpanded[0]. Due to implementation limitations, it currently cannot validate that the supplied IDs actually pertain to an accordion item and intelligently pick the first valid ID.

  • getIsExpanded(id) = true
  • getIsDisabled(id) = false

  • getIsExpanded(id) = true
  • getIsDisabled(id) = true

  • getIsExpanded(id) = false
  • getIsDisabled(id) = false
import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

const ITEMS = [ 'item1', 'item2', 'item3' ];

function InitializeStateAccordion() {
  return (
    <Accordion
      headerLevel={ 4 }
      initialExpanded={ [ 'item1', 'item2' ] }
      initialDisabled={ [ 'item2' ] }
      { ...props }
    >
    { ITEMS.map((id, index) => (
      <AccordionItem key={ id } id={ id }>
        <AccordionHeader>
          Accordion Item { index + 1 }
        </AccordionHeader>
        <AccordionPanel>
          { ({ id, getIsExpanded, getIsDisabled }) => (
            <ul className="mb-4">
              <li><code>getIsExpanded(id)</code> = <code>{ getIsExpanded(id).toString() }</code></li>
              <li><code>getIsDisabled(id)</code> = <code>{ getIsDisabled(id).toString() }</code></li>
            </ul>
          ) }
        </AccordionPanel>
      </AccordionItem>
    )) }
    </Accordion>
  );
}

Focusing Items

<AccordionHeader> automatically attaches a keydown event that implements the keyboard/focus behavior described in the APG. Try tabbing to one of the buttons and pressing the ArrowDown, ArrowUp, Home, or End keys. Additionally, you can manually focus accordion items through the methods provided by React ARIA Widgets.

import { useState } from 'react';
import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

const ITEMS = [ 'item1', 'item2', 'item3' ];

function FocusAccordion() {
  return (
    <Accordion headerLevel={ 4 }>
      { ITEMS.map((id, index) => (
        <AccordionItem key={ id } id={ id }>
          <AccordionHeader>
            Accordion Item { index + 1 }: ID = <code>{ id }</code>
          </AccordionHeader>
          <AccordionPanel>
            { (args) => <FocusForm { ...args } /> }
          </AccordionPanel>
        </AccordionItem>
      )) }
    </Accordion>
  );
}

function FocusForm({
  id,
  focusItemId,
  focusPrevItem,
  focusNextItem,
  focusFirstItem,
  focusLastItem,
}) {
  const [ inputItemId, setInputItemId ] = useState('');

  return (
    <form className="mb-4" onSubmit={ (e) => { e.preventDefault(); focusItemId(inputItemId); } }>
      <div className="field is-horizontal">
        <div className="field-label is-normal">
          <label className="label" htmlFor={ `${id}-focus-input` }>
            Item ID:
          </label>
        </div>
        <div className="field-body">
          <div className="field">
            <div className="control">
              <input
                id={ `${id}-focus-input` }
                type="text"
                onChange={ (e) => setInputItemId(e.target.value) }
                className="input"
              />
            </div>
          </div>
          <div className="field">
            <div className="control">
              <button className="button is-primary" type="submit">
                Focus Item
              </button>
            </div>
          </div>
        </div>
      </div>
      <div className="field is-grouped is-grouped-centered">
        <div className="control">
          <button className="button" type="button" onClick={ () => focusFirstItem() }>
            Focus First Item
          </button>
        </div>
        <div className="control">
          <button className="button" type="button" onClick={ () => focusPrevItem(id) }>
            Focus Previous Item
          </button>
        </div>
        <div className="control">
          <button className="button" type="button" onClick={ () => focusNextItem(id) }>
            Focus Next Item
          </button>
        </div>
        <div className="control">
          <button className="button" type="button" onClick={ () => focusLastItem() }>
            Focus Last Item
          </button>
        </div>
      </div>
    </form>
  );
}

Callbacks on State Changes

You can pass callback functions that fire after state changes:

  • onToggleExpanded - receives the items that are expanded
  • onToggleDisabled - receives the items that are disabled
  • onFocusItem - receives the item that was focused

Note that onFocusItem doesn't trigger for focus events in general, but rather, when React ARIA Widgets' focus methods are called. In other words, tabbing to a button won't trigger it, but pressing ArrowDown will.

Try opening your browser's developer tools and playing with the example below.

import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

const ITEMS = [ 'item1', 'item2', 'item3' ];

function CallbackAccordion() {
  return (
    <Accordion
      headerlevel={ 4 }
      onToggleExpanded={ expandedItems => console.log(expandedItems) }
      onToggleDisabled={ disabledItems => console.log(disabledItems) }
      onFocusChange={ ({ elem, index, id }) => console.log(elem, index, id) }
    >
      { ITEMS.map((id, index) => (
        <AccordionItem key={ id } id={ id }>
          <AccordionHeader>
            { ({ id, getIsDisabled }) => (
              <>
                Accordion Item { index + 1 }: Disabled = <code>{ getIsDisabled(id).toString() }</code>
              </>
            ) }
          </AccordionHeader>
          <AccordionPanel>
            { ({ id, toggleDisabled, getIsDisabled }) => (
              <button className="button is-primary mb-4" type="button" onClick={ () => toggleDisabled(id) }>
                { getIsDisabled(id) ? 'Enable' : 'Disable' } Item
              </button>
            ) }
          </AccordionPanel>
        </AccordionItem>
      )) }
    </Accordion>
  );
}

Controlling State

As previously demonstrated, state can be manually controlled from "below" the accordion by using render props. One could also create custom implementations of <AccordionHeader or <AccordionPanel> by using the hooks useAccordionContext and useAccordionItemContext.

State can be controlled from "above" the accordion by using useAccordion, a hook that provides methods to manage the state, and <ControlledAccordion>, a thin wrapper over the context provider that passes those methods down. Unlike <Accordion>, <ControlledAccordion> doesn't call useAccordion internally, allowing you to choose where to use it.

Expand/Collapse Items
Enable/Disable Items
Focus Items

Hello world!

Hello world!

Hello world!

import { useAccordion, ControlledAccordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

const ITEMS = [ 'item1', 'item2', 'item3' ];

function RemoteControlAccordion(props) {
  const contextValue = useAccordion(props);
  const { toggleExpanded, toggleDisabled, focusItemId, getIsDisabled } = contextValue;
  const toggleExpandButtons = [];
  const toggleDisableButtons = [];
  const focusButtons = [];
  const accordionItems = [];

  ITEMS.forEach((id, index) => {
    toggleExpandButtons.push(
      <div className="control" key={ id }>
        <button
          type="button"
          value={ id }
          onClick={ (e) => toggleExpanded(e.currentTarget.value) }
          className="button is-primary"
        >
          Expand/Collapse { id }
        </button>
      </div>
    );

    toggleDisableButtons.push(
      <div className="control" key={ id }>
        <button
          type="button"
          value={ id }
          onClick={ (e) => toggleDisabled(e.currentTarget.value) }
          className="button is-primary"
        >
          Enable/Disable { id }
        </button>
      </div>
    );

    focusButtons.push(
      <div className="control" key={ id }>
        <button
          type="button"
          value={ id }
          onClick={ (e) => focusItemId(e.currentTarget.value) }
          className="button is-primary"
        >
          Focus { id }
        </button>
      </div>
    );
    
    accordionItems.push(
      <AccordionItem id={ id } key={ id }>
        <AccordionHeader>
          Accordion Item { index + 1 }: Disabled = <code>{ getIsDisabled(id).toString() }</code>
        </AccordionHeader>
        <AccordionPanel>
          <p className="mb-4">Hello world!</p>
        </AccordionPanel>
      </AccordionItem>
    );
  });

  return (
    <>
      <form onSubmit={ e => e.preventDefault() } style={{ paddingBottom: '1rem' }}>
        <fieldset className="field is-grouped">
          <legend className="has-text-weight-semibold">Expand/Collapse Items</legend>
          { toggleExpandButtons }
        </fieldset>
        <fieldset className="field is-grouped">
          <legend className="has-text-weight-semibold">Enable/Disable Items</legend>
            { toggleDisableButtons }
          </fieldset>
        <fieldset className="field is-grouped">
          <legend className="has-text-weight-semibold">Focus Items</legend>
          { focusButtons }
        </fieldset>
      </form>
      <ControlledAccordion contextValue={ contextValue }>
        { accordionItems }
      </ControlledAccordion>
    </>
  );
}

Styling

There are a few different ways to style <AccordionHeader> and <AccordionPanel>:

  • Write CSS that targets the default classes applied by React ARIA Widgets
  • Supply a function for className that receives state and returns a string
  • Supply a string for className
  • Supply a function for style that receives state and returns an object
  • Supply an object for style

And, as previously alluded to, you can dynamically style your content by using a render function to render your content.

There are a few things to note when styling your content:

  • If you supply a string or function for className, it will replace the default class rather than append to it
  • React ARIA Widgets exposes the accordion's state onto the DOM using various HTML attributes, allowing them to be targeted with CSS selectors
  • If you wish you use the hidden attribute to collapse your panels, unfortunately React ARIA Widgets currently does not have a good API for doing so. You can create your own accordion panel implementation using the hooks provided, but that's an admittedly awkward workaround. It's arguable though that in most cases, using display: none; has better semantics than hidden. For more information, see the FAQ.
ComponentHTML ElementDefault CSS ClassState SelectorSelector Description
<AccordionHeader><h1> to <h6>react-aria-widgets-accordion-header[data-expanded=true | false]Whether the panel is expanded or collapsed.
[data-disabled=true | false]Whether toggling visibility is enabled or disabled.
<AccordionHeader><button>react-aria-widgets-accordion-button[aria-expanded=true | false]Whether the panel is expanded or collapsed.
[aria-disabled=true | false]Whether toggling visibility is enabled or disabled.
<AccordionPanel><section> by defaultreact-aria-widgets-accordion-panel[data-expanded=true | false]Whether the panel is expanded or collapsed.
[data-disabled=true | false]Whether toggling visibility is enabled or disabled.

The rendered markup looks something like this:

<h1 class="react-aria-widgets-accordion-header" data-expanded="false" data-disabled="false">
  <button class="react-aria-widgets-accordion-button" aria-expanded="false" aria-disabled="false">
    Accordion Item Header
  </button>
</h1>
<section class="react-aria-widgets-panel" data-expanded="false" data-disabled="false">
  Hello world!
</section>

In the following example, each of the accordion items are styled using one of the provided methods.

This accordion item is styled by CSS that targets the default classes provided by React ARIA Widgets. Since React ARIA Widgets also exposes the accordion's state via HTML data attributes, we can target selectors such as [data-expanded] or [data-disabled].

This accordion item is styled by passing in strings for className and CSS that targets the supplied classes and the state exposed by React ARIA Widgets.

This accordion item is styled by passing in objects for style.

import { Accordion, AccordionItem, AccordionHeader, AccordionPanel } from 'react-aria-widgets/accordion';

function StyledAccordion() {
  return (
    <Accordion headerLevel={ 4 }>
      <AccordionItem id="item1">
        <AccordionHeader>
          Accordion Item 1
        </AccordionHeader>
        <AccordionPanel>
          <p>
            This accordion item is styled by CSS that targets the default classes provided by React ARIA
            Widgets. Since React ARIA Widgets also exposes the accordion&apos;s state via HTML data attributes,
            we can target selectors such as <code>[data-expanded]</code> or <code>[data-disabled]</code>.
          </p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item2">
        <AccordionHeader
          headerProps={{ className: 'custom-accordion-header' }}
          buttonProps={{ className: 'custom-accordion-button' }}
        >
          Accordion Item 2
        </AccordionHeader>
        <AccordionPanel className="custom-accordion-panel">
          <p> 
            This accordion item is styled by passing in strings for <code>className</code> and
            CSS that targets the supplied classes and the state exposed by React ARIA Widgets.
          </p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item3">
        <AccordionHeader
          headerProps={{ style: { color: 'hsl(217, 71%, 45%)' } }}
          buttonProps={{ style: { color: 'inherit' } }}
        >
          Accordion Item 3
        </AccordionHeader>
        <AccordionPanel style={{ color: 'hsl(217, 71%, 45%)' }}>
          <p className="mb-4">
            This accordion item is styled by passing in objects for <code>style</code>.
          </p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item4">
        <AccordionHeader
          headerProps={{ className: ({ isExpanded }) => `another-custom-header ${isExpanded ? 'expanded' : 'collapsed'}` }}
          buttonProps={{ className: ({ isExpanded }) => `another-custom-button ${isExpanded ? 'expanded' : 'collapsed'}` }}
        >
          Accordion Item 4
        </AccordionHeader>
        <AccordionPanel className={ ({ isExpanded }) => `another-custom-panel ${isExpanded ? 'expanded' : 'collapsed'}` }>
          <p>
            This accordion item is styled by passing in functions for <code>className</code>. These functions
            have access to the accordion&apos;s state, allowing you to dynamically apply classes.
          </p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item5">
        <AccordionHeader
          headerProps={{ style: ({ isExpanded }) => isExpanded ? { color: 'hsl(0, 0%, 100%)' } : {} }}
          buttonProps={{ style: ({ isExpanded }) => isExpanded ? { color: 'inherit', backgroundColor: 'hsl(217, 71%, 53%' } : {} }}
        >
          Accordion Item 5
        </AccordionHeader>
        <AccordionPanel style={ ({ isExpanded }) => isExpanded ? {} : { display: 'none' } }>
          <p className="mb-4">
            This accordion item is styled by passing in functions for <code>style</code>. As before, these
            functions allow you to dynamically apply styles based on the accordion&apos;s state.
          </p>
        </AccordionPanel>
      </AccordionItem>
      <AccordionItem id="item6">
        <AccordionHeader>
          { ({ id, getIsExpanded }) => (
            <span
              className={ getIsExpanded(id) ? 'expanded' : 'collapsed' }
              style={ getIsExpanded(id) ? { color: 'hsl(217, 71%, 45%)' } : {} }
            >
              Accordion Item 6
            </span>
          ) }
        </AccordionHeader>
        <AccordionPanel>
          { ({ id, getIsExpanded }) => (
            <p
              className={ getIsExpanded(id) ? 'expanded' : 'collapsed' }
              style={ getIsExpanded(id) ? { color: 'hsl(217, 71%, 45%)' } : {} }
            >
              The content for this accordion item is rendered with a render function. Since these render
              functions have access to the accordion&apos;s state, you can dynamically style your content.
            </p>
          ) }
        </AccordionPanel>
      </AccordionItem>
    </Accordion>
  );
}
.react-aria-widgets-accordion-panel[data-expanded=false] {
  display: none;
}

.custom-accordion-panel[data-expanded=false] {
  display: none;
}

.another-custom-panel.collapsed {
  display: none;
}

Further Customization

React ARIA Widgets exposes all of the hooks, contexts, components, etc. that it uses, allowing you to create your own accordion implementations.

Creating a Custom <Accordion>

As mentioned in Controlling State, one can simply combine useAccordion and <ControlledAccordion> to create a new version of <Accordion>.

import { useAccordion, ControlledAccordion } from 'react-aria-widgets/accordion';

function CustomAccordion({
  children = null,
  ...rest
}) {
  const contextValue = useAccordion(rest);

  return (
    <ControlledAccordion contextValue={ contextValue }>
      { children }
    </ControlledAccordion>
  );
}

Creating Custom Headers and Panels

You can use the hook useAccordionContext to read and modify the accordion state that gets sent down from <ControlledAccordion>. We'll be using it to create our own accordion headers and panels.

We won't be using them in this example, but for the sake of convenience, React ARIA Widgets also provides the components <BaseAccordionHeader> and <BaseAccordionPanel> to simplify building your own accordions. They're essentially just thin wrappers over HTML, but they use TypeScript and PropTypes to help remind you what HTML attributes are needed to fulfill the APG.

import { useAccordionContext } from 'react-aria-widgets/accordion';

function CustomAccordionHeader({ children = null, id }) {
  const {
    headerLevel,
    getIsExpanded,
    getIsDisabled,
    toggleExpanded,
    pushItemRef,
    focusPrevItem,
    focusNextItem,
    focusFirstItem,
    focusLastItem,
  } = useAccordionContext();
  const HeaderElement = `h${headerLevel}`;
  const isExpanded = getIsExpanded(id);
  const isDisabled = getIsDisabled(id);

  const refCallback = (ref) => {
    pushItemRef(ref, id);
  };

  const onClick = () => {
    toggleExpanded(id);
  };

  const onKeyDown = (event) => {
    const { key } = event;

    if(key === 'ArrowUp') {
      event.preventDefault();
      focusPrevItem(id);
    }
    else if(key === 'ArrowDown') {
      event.preventDefault();
      focusNextItem(id);
    }
    else if(key === 'Home') {
      event.preventDefault();
      focusFirstItem();
    }
    else if(key === 'End') {
      event.preventDefault();
      focusLastItem();
    }
  };

  return (
    <HeaderElement className="my-accordion-header">
      <button
        type="button"
        className="button is-primary my-accordion-button is-flex is-align-items-baseline has-text-right"
        id={ id }
        onClick={ onClick }
        onKeyDown={ onKeyDown }
        aria-controls={ `${id}-panel` }
        aria-expanded={ isExpanded }
        aria-disabled={ isDisabled }
        ref={ refCallback }
      >
        { children }
        <i
          className={ `fa-solid fa-chevron-${isExpanded ? 'down' : 'right'} is-flex-grow-1` }
          aria-hidden="true"
        />
      </button>
    </HeaderElement>
  );
}
import { useAccordionContext } from 'react-aria-widgets/accordion';

function CustomAccordionPanel({ children = null, id }) {
  const { getIsExpanded } = useAccordionContext();
  const isExpanded = getIsExpanded(id);

  return (
    <section
      id={ `${id}-panel` }
      aria-labelledby={ id }
      className={ `my-accordion-panel ${isExpanded ? 'expanded' : 'collapsed'} content` }
    >
      { children }
    </section>
  );
}
.my-accordion-header {
  margin-bottom: 1rem;
}

.my-accordion-button {
  width: 100%;
}

.my-accordion-panel.collapsed {
  display: none;
}

Putting It All Together

You'll notice that we didn't create another version of <AccordionItem>. Its main job is to make sure that the header and panel both have the same ID, and we won't need that in this example.

Here's the completed accordion:

import CustomAccordion from './CustomAccordion';
import CustomAccordionHeader from './CustomAccordionHeader';
import CustomAccordionPanel from './CustomAccordionPanel';

function MyAccordion(props) {
  return (
    <CustomAccordion { ...props }>
      <CustomAccordionHeader id="item1">
        Joke #1
      </CustomAccordionHeader> 
      <CustomAccordionPanel id="item1">
        <p>Why don&apos;t scientists trust atoms? Because they make up everything!</p>
      </CustomAccordionPanel>
      <CustomAccordionHeader id="item2">
        Joke #2 
      </CustomAccordionHeader> 
      <CustomAccordionPanel id="item2">
        Why did the bicycle fall over? Because it was two tired!
      </CustomAccordionPanel>
      <CustomAccordionHeader id="item3">
        Joke #3
      </CustomAccordionHeader> 
      <CustomAccordionPanel id="item3">
        What do you call fake spaghetti? An &quot;impasta&quot;!
      </CustomAccordionPanel>
    </CustomAccordion>
  );
}

API

Components

<Accordion>

Provides accordion state and functionality to its constituent components. It passes this data down to child components via the context API, which can be read with the hook useAccordionContext.

Props

The type definition for this component's props are exported as AccordionProps.

NameTypeDefault ValueRequiredDescription
childrenReact.ReactNodenullReact nodes that represent the accordion's constituent headers and panels. Does not have to be the components provided by React ARIA Widgets.
allowMultiplebooleantrueDetermines whether or not multiple accordion items can be expanded at the same time.
allowCollapseLastbooleantrueDetermines whether or not the last expanded panel can be collapsed.
headerLevel1 | 2 | 3 | 4 | 5 | 6YesDetermines the HTML heading element (e.g. <h1>) of each accordion header.
initialExpandedstring[][]Determines which accordion items (identified by their ID) should be expanded on the initial mount. If allowMultiple is off, the first element in the array is picked naively.
initialDisabledstring[][]Determines which accordion items (identified by their ID) should be prevented from expanding or collapsing on the initial mount.
onToggleExpanded(expandedItems: Set<string>) => void;undefinedCallback to be fired after an item is expanded or collapsed. Receives the currently-expanded item IDs as an argument.
onToggleDisabled(disabledItems: Set<string>) => void;undefinedCallback to be fired after an item is enabled/disabled. Receives the currently-disabled item IDs as an argument.
onFocusItem(args: { elem: HTMLButtonElement | HTMLElement | null; index: number; id: string; }) => void;undefinedCallback to be fired after an item receives focus. Note that this only runs when using one of the focus methods provided by useAccordion.

<AccordionItem>

Represents a header/panel pair. Helps ensure that they both have the same ID and generates HTML IDs for attributes like id and aria-labelledby. Passes information down to child components via the context API, which can be read with the hook useAccordionItemContext.

Props

The type definition for this component's props are exported as AccordionItemProps.

NameTypeDefault ValueRequiredDescription
childrenReact.ReactNodenullTechnically allows for anything renderable by React, but you should pass in components that represent the accordion's header and panel (e.g. <AccordionHeader> and <AccordionPanel>).
idstringYesA string that uniquely identifies this header/panel pair. IDs do not have to be unique globally, but they do have to be unique amongst its siblings.

<ControlledAccordion>

Acts similarly to <Accordion> in that its role is to act as a context provider for the accordion's fields and methods. However, unlike <Accordion>, it doesn't use the useAccordion hook, giving you the freedom to choose where to use it.

Props

The type definition for this component's props are exported as ControlledAccordionProps.

NameTypeDefault ValueRequiredDescription
contextValueAccordionMembersYesThe object returned by useAccordion (i.e. the accordion's fields and methods).

<AccordionHeader>

Represents the header of an accordion. Receives the fields and methods from the accordion contexts by using the useAccordionContext and useAccordionItemContext hooks. Implements event handlers to manage focus and expand/collapse its panel's visibility. Also sets the HTML/ARIA attributes needed to fulfill the APG.

Props

The type definition for this component's props are exported as AccordionHeaderProps.

NameTypeDefault ValueRequiredDescription
childrenReact.ReactNode | AccordionRenderFunctionnull

The content to be rendered. This can either be a string, component, etc., or a render function.

If you provide a render function, it will receive all of the fields and methods provided by useAccordionContext and useAccordionItemContext.

Note that because the content is placed inside of a <button>, it must not contain any interactive content or an element with the tabindex attribute specified. See the <button> specification for more information.

headerPropsAccordionHeaderElementProps{}

An object that is spread onto the underlying HTML heading element, allowing you to pass props and attributes to it.

You can supply a string or CSSProperties object for headerProps.className or headerProps.style respectively, or you can dynamically apply styles by providing a function. This function will receive accordion state information (see AccordionRenderStyleData) and should return a string or CSSProperties object.

If no className property is supplied, the default value will be react-aria-widgets-accordion-header. If no style property is supplied, the default value will be undefined.

buttonPropsAccordionButtonElementProps{}

An object that is spread onto the underlying HTML button element, allowing you to pass props and attributes to it.

You can supply a string or CSSProperties object for buttonProps.className or buttonProps.style respectively, or you can dynamically apply styles by providing a function. This function will receive accordion state information (see AccordionRenderStyleData) and should return a string or CSSProperties object.

If no className property is supplied, the default value will be react-aria-widgets-accordion-button. If no style property is supplied, the default value will be undefined.

Data Attributes
HTML ElementAttributeValues
<h1> to <h6>[data-expanded]true | false
[data-disabled]true | false
<button>[aria-expanded]true | false
[aria-disabled]true | false

<AccordionPanel>

Represents the body of content for an accordion item. Receives the fields and methods from the accordion contexts by using the useAccordionContext and useAccordionItemContext hooks. Sets the HTML/ARIA attributes needed to fulfill the APG.

Props

Note that if you pass any props other than those listed below, they will be spread onto the underlying element (indicated by the as prop).

The type definitions for this component's props are exported as AccordionPanelProps.

NameTypeDefault ValueRequiredDescription
childrenReact.ReactNode | AccordionRenderFunctionnull

The content to be rendered. This can either be a string, component, etc., or a render function.

If you provide a render function, it will receive all of the fields and methods provided by useAccordionContext and useAccordionItemContext.

classNamestring | AccordionRenderClassreact-aria-widgets-accordion-panelA string or function that determines the CSS class. If you supply a function, it will receive state information (see AccordionRenderStyleData) that will allow you to dynamically set the class.
styleReact.CSSProperties | AccordionRenderStyle{}An object or function that determines the style attribute. If you supply a function, it will receive state information (see AccordionRenderStyleData) that will allow you to dynamically apply styles.
asReact.ElementType'section'

Determines the element that will be rendered.

Note that the default element <section> has the region role, and that there are times where this may be undesireable. The APG advises:

Avoid using the region role in circumstances that create landmark region proliferation, e.g. in an accordion that contains more than approximately 6 panels that can be expanded at the same time.
Data Attributes
AttributeValues
[data-expanded]true | false
[data-disabled]true | false

<BaseAccordionHeader>

A stateless component that represents an accordion header. Exists mainly to provide guardrails to help ensure adherence to the APG, namely:

  • The heading element contains only a button
  • The content lives in the button
  • Uses TypeScript and PropTypes to remind developers which HTML/ARIA attributes need to be set
Props

The type definition for this component's props are exported as BaseAccordionHeaderProps.

NameTypeDefault ValueRequiredDescription
childrenReact.ReactNodenull

A string, React component, etc., to be rendered.

Note that because the content is placed inside of a <button>, it must not contain any interactive content or an element with the tabindex attribute specified. See the <button> specification for more information.

idstringundefined(See description)

The HTML ID for the button element.

Note that if the HTML element representing the corresponding accordion panel has the region role, then the panel must be labeled. This is ideally done by giving the panel an aria-labelledby attribute that points to the button.

Accordion panels in general are not required to have the region role, but the panel components provided by React ARIA Widgets default to <section>, which does have that role. In other words, if you use them with this component, the id will be required by default.

headerLevel1 | 2 | 3 | 4 | 5 | 6YesDetermines the HTML heading element (e.g. <h1>).
onClickReact.MouseEventHandler<HTMLButtonElement>YesA click event handler for the button. Should handle expanding/collapsing the panel.
onKeyDownReact.KeyboardEventHandler<HTMLButtonElement>undefinedA keydown event handler for the button. Can be used to provide focus management.
aria-controlsstringYesA unique identifier that points to the accordion panel's HTML ID.
aria-expandedbooleanYes

Informs assistive technologies whether or not the panel is expanded.

Note that this attribute does not affect the visibility of the panel.

aria-disabledbooleanYes

Informs assistive technologies if the button cannot be interacted with. A common use-case would be if the associated panel is currently expanded, and the accordion does not allow it to be collapsed (e.g. allowCollapseLast is off and there's only one expanded panel).

Note that unlike the disabled attribute, aria-disabled does not actually disable any behaviors such as preventing events from triggering. See the MDN Web Docs for more information.

headerPropsBaseAccordionHeaderElementProps{}An object that is spread onto the underlying heading element, allowing you to pass props and attributes to it.
buttonPropsBaseAccordionButtonElementProps{}An object that is spread onto the underlying button element, allowing you to pass props and attributes to it.

<BaseAccordionPanel>

A stateless component that represents an accordion panel. Exists mainly to provide guardrails to help ensure adherence to the APG, namely by using TypeScript and PropTypes to remind developers which HTML/ARIA attributes need to be set.

Props

Note that if you pass any props other than those listed below, they will be spread onto the underlying element (indicated by the as prop).

The type definition for this component's props are exported as BaseAccordionPanelProps.

NameTypeDefault ValueRequiredDescription
childrenReact.ReactNodenullThe content to be rendered.
asReact.ElementType'section'

Determines the element that will be rendered. Note that the default element, <section>, has the region role, which has a couple implications.

First, elements with the region role must be labeled, making aria-labelledby required by default.

Second, there are times where the region role may be undesirable. The APG advises:

Avoid using the region role in circumstances that create landmark region proliferation, e.g. in an accordion that contains more than approximately 6 panels that can be expanded at the same time.
idstringYesThe HTML ID for the panel. Note that the corresponding accordion header button should also have an aria-controls attribute that points to this panel.
aria-labelledbystringundefinedYes by default, see description for details.

A string that points to the accordion header button's HTML ID.

If the HTML element representing the accordion panel has the region role, then it must be labeled. Accordion panels in general are not required to have this role, but the default element for this component, <section>, does have the region role. Therefore, this prop is required by default.

Hooks

useAccordion

useAccordion is the hook that provides the state and functionality for the accordion. It accepts a number of arguments that help determine the behavior of the accordion, and returns fields and methods that get or set the state.

Arguments

This hook accepts an object of type UseAccordionOptions that contains the following properties:

NameTypeDefault ValueRequiredDescription
allowMultiplebooleantrueControls whether or not multiple panels can be expanded at the same time.
allowCollapseLastbooleantrueControls whether or not the last expanded panel can be collapsed.
headerLevel1 | 2 | 3 | 4 | 5 | 6YesDetermines the HTML heading element (e.g. <h1>) of each accordion header.
initialExpandedstring[][]Determines which accordion items (identified by their ID) should be expanded on the initial mount. If allowMultiple is off, the hook naively picks the first element in the array.
initialDisabledstring[][]Determines which accordion items (identified by their ID) should be prevented from expanding or collapsing on the initial mount.
onToggleExpanded(expandedItems: Set<string>) => void;undefinedCallback to be fired after an item is expanded or collapsed. Receives the currently-expanded item IDs as an argument.
onToggleDisabled(disabledItems: Set<string>) => void;undefinedCallback to be fired after an item is enabled/disabled. Receives the currently-disabled item IDs as an argument.
onFocusItem(args: { elem: HTMLButtonElement | HTMLElement | null; index: number; id: string; }) => void;undefinedCallback to be fired after an item receives focus. Note that this only runs when using one of the focus methods provided by this hook.
Return Value

The type of the returned object is exported as AccordionMembers.

NameTypeDescription
allowMultiplebooleanInforms downstream components whether or not multiple panels can be expanded at the same time.
allowCollapselastbooleanInforms downstream components whether or not the last expanded panel can be collapsed.
headerLevel1 | 2 | 3 | 4 | 5 | 6Determines the HTML heading element (e.g. <h1>) of each accordion header.
getIsExpanded(id: string) => booleanReturns whether an accordion item is currently expanded.
getIsDisabled(id: string) => booleanReturns whether an accordion item is currently prevented from being expanded/collapsed.
toggleExpanded(id: string) => voidExpands/collapses an accordion item.
toggleDisabled(id: string) => voidPrevents/allows an accordion item from being expanded/collapsed.
pushItemRef(elem: HTMLButtonElement | HTMLElement | null, id: string) => void;Registers an accordion item to the hook. The hook must be aware of each header button in the accordion to manage focus.
focusItemIndex(index: number) => voidFocuses an accordion item based on its index.
focusItemId(id: string) => voidFocuses an accordion item based on its ID.
focusPrevItem(id: string) => voidFocuses the previous accordion item (relative to the supplied ID).
focusNextItem(id: string) => voidFocuses the next accordion item (relative to the supplied ID).
focusFirstItem() => voidFocuses the first accordion item.
focusLastItem() => voidFocuses the last accordion item.

useAccordionContext

<Accordion> and <ControlledAccordion> pass down the fields and methods from useAccordion via the context API. This hook can be used to read from that context provider.

Arguments

This hook doesn't accept any arguments.

Return Value

This hook has the same return value as useAccordion's return value, an object of type AccordionMembers.

useAccordionItemContext

<AccordionItem> passes down the IDs for its header and panel via the context API. This hook can be used to read from that context provider.

Arguments

This hook doesn't accept any arguments.

Return Value

This hook returns an object of type AccordionItemContextType that contains the following properties:

NameTypeDescription
idstringThe accordion item's identifier. Should be unique amongst its sibling items.
headerHTMLIdstringA string ID used to identify the accordion header button via attributes like id and aria-labelledby.
panelHTMLIdstringA string ID used to identify the accordion panel via attributes like id and aria-controls.

Contexts

AccordionContext

This context provides the fields and methods from useAccordion to consumers like useAccordionContext. Chances are, you'll be using <Accordion> or <ControlledAccordion> instead of importing this directly, but React ARIA Widgets exports it for those who wish to use it.

AccordionContext.Provder is also exported as AccordionProvider for those who prefer it for aesthetic or other reasons.

AccordionItemContext

This context provides the header and panel IDs from <AccordionItem> to consumers like useAccordionItemContext. Chances are, you'll be using <AccordionItem> instead, but React ARIA Widgets exports it for those who wish to use it.

AccordionItemContext.Provider is also exported as AccordionItemProvider for those who prefer it for aesthetic or other reasons.

Types

NameDefinition
AccordionProps
type AccordionProps = React.PropsWithChildren<UseAccordionOptions>;
AccordionItemProps
type AccordionItemProps = React.PropsWithChildren<{ id: string }>;
ControlledAccordionProps
type CustomAccordionProps = React.PropsWithChildren<{
  contextValue: AccordionMembers;
}>;
AccordionHeaderProps
interface AccordionHeaderProps {
  children?: React.ReactNode | AccordionRenderFunction;
  headerProps?: AccordionHeaderElementProps;
  buttonProps?: AccordionButtonElementProps;
}
AccordionHeaderElementProps
type AccordionHeaderElementProps = {
  className?: string | AccordionRenderClass;
  style?: React.CSSProperties | AccordionRenderStyle;
} & Omit<
  BaseAccordionHeaderElementProps,
  'className' | 'style'
>;
AccordionButtonElementProps
type AccordionButtonElementProps = {
  className?: string | AccordionRenderClass;
  style?: React.CSSProperties | AccordionRenderStyle;
} & Omit<
  BaseAccordionButtonElementProps,
  'className' | 'style'
>;
AccordionPanelProps
type AccordionPanelProps<
  C extends React.ElementType = 'section'
> = PolymorphicComponentPropsWithoutRef<
  C,
  {
    children?: React.ReactNode | AccordionRenderFunction;
    className?: string | AccordionRenderClass;
    style?: React.CSSProperties | AccordionRenderStyle;
  }
>;
BaseAccordionHeaderProps
type BaseAccordionHeaderProps = React.PropsWithChildren<{
  id?: string;
  headerLevel: 1 | 2 | 3 | 4 | 5 | 6;
  onClick: React.MouseEventHandler<HTMLButtonElement>;
  onKeyDown?: React.KeyboardEventHandler<HTMLButtonElement>;
  'aria-controls': string;
  'aria-expanded': boolean;
  'aria-disabled': boolean;
  headerProps?: BaseAccordionHeaderElementProps;
  buttonProps?: BaseAccordionButtonElementProps;
}>;
BaseAccordionHeaderElementProps
type BaseAccordionHeaderElementProps = Omit<
  React.HTMLAttributes<HTMLHeadingElement>,
  'children' | 'dangerouslySetInnerHTML'
>;
BaseAccordionButtonElementProps
type BaseAccordionButtonElementProps = Omit<
  React.ButtonHTMLAttributes<HTMLButtonElement>,
  'children' |
  'dangerouslySetInnerHTML' |
  'type' |
  'id' |
  'aria-controls' |
  'onClick' |
  'onKeyDown' |
  'aria-expanded' |
  'aria-disabled'
>;
BaseAccordionPanelProps
type BaseAccordionPanelProps<
  C extends React.ElementType = 'section'
> = PolymorphicComponentPropsWithRef<
  C,
  {
    id: string;
    'aria-labelledby'?: string;
  }
>;
UseAccordionOptions
interface UseAccordionOptions {
  allowMultiple?: boolean;
  allowCollapseLast?: boolean;
  headerLevel: 1 | 2 | 3 | 4 | 5 | 6;
  initialExpanded?: string[];
  initialDisabled?: string[];
  onToggleExpanded?: (expandedItems: Set<string>) => void;
  onToggleDisabled?: (disabledItems: Set<string>) => void;
  onFocusChange?: ({
    elem,
    index,
    id
  }: {
    elem: HTMLButtonElement | HTMLElement | null;
    index: number;
    id: string;
  }) => void;
}
AccordionMembers
type AccordionMembers = ReturnType<typeof useAccordion>;
AccordionItemContextType
interface AccordionItemContextType {
  id: string;
  headerHTMLId: string;
  panelHTMLId: string;
}
AccordionRenderFunction
type AccordionRenderFunction = (args: AccordionMembers & AccordionItemContextType) => React.ReactNode;
AccordionRenderClass
type AccordionRenderClass = (args: AccordionRenderStyleData) => string;
AccordionRenderStyle
type AccordionRenderStyle = (args: AccordionRenderStyleData) => React.CSSProperties;
AccordionRenderStyleData
interface AccordionRenderStyleData {
  allowMultiple: boolean;
  allowCollapseLast: boolean;
  headerLevel: 1 | 2 | 3 | 4 | 5 | 6;
  isExpanded: boolean;
  isDisabled: boolean;
}