Building an Accordion Component with React

joshtronic

Ahhhhh, the accordion. I’m not talking about Weird Al’s instrument of choice, but the graphical control element. Do you have too much content on a page? An accordion allows you to break things up into smaller chunks that your end-users can toggle open and close by clicking.

Similar to a tabs component, an accordion component is comprised of different sections that can be toggled to be open and closed. Unlike a tabs component, accordions can support displaying multiple sections of content at the same time.

In this article, you will learn how to create a simple reusable accordion component with sections that can be toggled open and closed. We will then expand the component to support having multiple sections opened at once and specifying which sections should be open by default.

We will be creating three components:

  • Accordion which will hold our section components and manage which sections are open and closed.
  • AccordionSection which will display the clickable section title, the section contents when the section is open, and report back to the Accordion about click events.
  • App component to tie everything together into a working example!

The sections inside of the Accordion component will simply be <div> tags that will receive a label attribute that will be used for the clickable region inside of the AccordionSection component.


Let’s start by creating our innermost component, AccordionSection:

Component: AccordionSection.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';

class AccordionSection extends Component {
  static propTypes = {
    children: PropTypes.instanceOf(Object).isRequired,
    isOpen: PropTypes.bool.isRequired,
    label: PropTypes.string.isRequired,
    onClick: PropTypes.func.isRequired,
  };

  onClick = () => {
    this.props.onClick(this.props.label);
  };

  render() {
    const {
      onClick,
      props: { isOpen, label },
    } = this;

    return (
      <div
        style={{
          background: isOpen ? '#fae042' : '#6db65b',
          border: '1px solid #008f68',
          padding: '5px 10px',
        }}
      >
        <div onClick={onClick} style={{ cursor: 'pointer' }}>
          {label}
          <div style={{ float: 'right' }}>
            {!isOpen && <span>&#9650;</span>}
            {isOpen && <span>&#9660;</span>}
          </div>
        </div>
        {isOpen && (
          <div
            style={{
              background: '#6db65b',
              border: '2px solid #008f68',
              marginTop: 10,
              padding: '10px 20px',
            }}
          >
            {this.props.children}
          </div>
        )}
      </div>
    );
  }
}

export default AccordionSection;

This component will receive a label property that will create the clickable title of the section. The onClick event handler property is used to report back to the parent component if the label has been clicked.

The parent component will handle keeping track of which sections are opened or closed and feeds the status back to AccordionSection as a boolean property, isOpen.

Any child components of AccordionSection will be displayed only if the section has been toggled to open and the property isOpen is true.


Now that we have a component for our sections, we need a component that will house these sections and manage the state of which sections have been opened or closed:

Component: Accordion.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';

import AccordionSection from './AccordionSection';

class Accordion extends Component {
  static propTypes = {
    children: PropTypes.instanceOf(Object).isRequired,
  };

  constructor(props) {
    super(props);

    const openSections = {};

    this.state = { openSections };
  }

  onClick = label => {
    const {
      state: { openSections },
    } = this;

    const isOpen = !!openSections[label];

    this.setState({
      openSections: {
        [label]: !isOpen
      }
    });
  };

  render() {
    const {
      onClick,
      props: { children },
      state: { openSections },
    } = this;

    return (
      <div style={{ border: '2px solid #008f68' }}>
        {children.map(child => (
          <AccordionSection
            isOpen={!!openSections[child.props.label]}
            label={child.props.label}
            onClick={onClick}
          >
            {child.props.children}
          </AccordionSection>
        ))}
      </div>
    );
  }
}

export default Accordion;

Great! Our Accordion component will receive child nodes that will be used to create our interactive AccordionSection components.

This component tracks which section has been toggled opened but only keeps track of a single section being open at a time (for now).


Having created our Accordion and AccordionSection components, we can now create our App component and see things in action!

For our accordion demo, we will create an Accordion with two child components that contain some facts about Alligators:

Main: index.js

import React from 'react';
import { render } from 'react-dom';

import Accordion from './Accordion';

function App() {
  return (
    <div>
      <h1>Accordion Demo</h1>
      <Accordion>
        <div label='Alligator Mississippiensis'>
          <p>
            <strong>Common Name:</strong> American Alligator
          </p>
          <p>
            <strong>Distribution:</strong> Texas to North Carolina, US
          </p>
          <p>
            <strong>Endangered Status:</strong> Currently Not Endangered
          </p>
        </div>
        <div label='Alligator Sinensis'>
          <p>
            <strong>Common Name:</strong> Chinese Alligator
          </p>
          <p>
            <strong>Distribution:</strong> Eastern China
          </p>
          <p>
            <strong>Endangered Status:</strong> Critically Endangered
          </p>
        </div>
      </Accordion>
    </div>
  );
}

const container = document.createElement('div');
document.body.appendChild(container);
render(<App />, container);

Usual boilerplate code. The Accordion contains two sections that, upon being clicked open, will display some facts about different species of alligators.


Everything we have created thus far is usable, but somewhat limited as only a single section can be open at a time, and every section is collapsed by default.

To add support for multiple sections being open at the same time, we’re going to add a property named allowMultipleOpen to the Accordion component which will be used tell the accordion if it should allow multiple sections to be open.

When enabled, the state will toggle individual keys true/false instead of completely overwriting the state with the section that was interacted with.

While we’re in there, we can add some additional logic to check the child nodes for an isOpen property. When present, the open state will be initialized with the section being marked as open already:

Component: Accordion.js

import React, { Component } from 'react';
import PropTypes from 'prop-types';

import AccordionSection from './AccordionSection';

class Accordion extends Component {
  static propTypes = {
    allowMultipleOpen: PropTypes.bool,
    children: PropTypes.instanceOf(Object).isRequired,
  };

  constructor(props) {
    super(props);

    const openSections = {};

    this.props.children.forEach(child => {
      if (child.props.isOpen) {
        openSections[child.props.label] = true;
      }
    });

    this.state = { openSections };
  }

  onClick = label => {
    const {
      props: { allowMultipleOpen },
      state: { openSections },
    } = this;

    const isOpen = !!openSections[label];

    if (allowMultipleOpen) {
      this.setState({
        openSections: {
          ...openSections,
          [label]: !isOpen
        }
      });
    } else {
      this.setState({
        openSections: {
          [label]: !isOpen
        }
      });
    }
  };

  render() {
    const {
      onClick,
      props: { children },
      state: { openSections },
    } = this;

    return (
      <div style={{ border: '2px solid #008f68' }}>
        {children.map(child => (
          <AccordionSection
            isOpen={!!openSections[child.props.label]}
            label={child.props.label}
            onClick={onClick}
          >
            {child.props.children}
          </AccordionSection>
        ))}
      </div>
    );
  }
}

export default Accordion;

With our Accordion ready to accept new parameters, we can update our App component to pass in the property to allow multiple sections to be open as well as flagging the first section to be open by default:

Main: index.js

import React from 'react';
import { render } from 'react-dom';

import Accordion from './Accordion';

function App() {
  return (
    <div>
      <h1>Accordion Demo</h1>
      <Accordion allowMultipleOpen>
        <div label='Alligator Mississippiensis' isOpen>
          <p><strong>Common Name:</strong> American Alligator</p>
          <p><strong>Distribution:</strong> Texas to North Carolina, US</p>
          <p><strong>Endangered Status:</strong> Currently Not Endangered</p>
        </div>
        <div label='Alligator Sinensis'>
          <p><strong>Common Name:</strong> Chinese Alligator</p>
          <p><strong>Distribution:</strong> Eastern China</p>
          <p><strong>Endangered Status:</strong> Critically Endangered</p>
        </div>
      </Accordion>
    </div>
  );
}

const container = document.createElement('div');
document.body.appendChild(container);
render(<App />, container);

There you have it, a reusable accordion component to help you tame unruly content.

Did I mention that you could even nest these accordions to satisfy even the most complex scenarios?

You can find a working demo and code from this article over on CodeSandbox.

Enjoy! 💥

  Tweet It

🕵 Search Results

🔎 Searching...