DEV Community

Cover image for Building Components in React: Accordion
faisal khan
faisal khan

Posted on

Building Components in React: Accordion

An accordion is a vertically stacked list of headers that can be clicked to reveal or hide content associated with them.

It is one of many ways you can expose content to users in a progressive manner. Allowing people to have control over the content by expanding it or deferring it for later lets them decide what to read and what to ignore.

Let's build an ideal Accordion component in react from basics to advance


Let's code it


Libraries used: classnames, react-transition-group, material-ui/icons

Table Of Contents

1. Creating a basic accordion

import React, { useState } from 'react';
import classNames from 'classnames';

import ExpandMoreIcon from '@material-ui/icons/ExpandMore';

const Accordion = ({ children, isAlreadyOpen, title, id, onOpen, onClose }) => {
  const [isOpen, setIsOpen] = useState(isAlreadyOpen);

  const onToggle = () => {
    if (!isOpen) {
      onOpen(id);
    } else {
      onClose(id);
    }
    setIsOpen((currentState) => !currentState);
  };

  return (
    <section>
      <div
        onClick={onToggle}
        style={{
          display: 'flex',
          justifyContent: 'space-between',
          cursor: 'pointer',
        }}
      >
        <h4>{title}</h4>
        <span>
          <ExpandMoreIcon />
        </span>
      </div>
      {isOpen ? (
        <div>
          <section>
            <section>{children}</section>
          </section>
        </div>
      ) : null}
    </section>
  );
};

export default Accordion;
Enter fullscreen mode Exit fullscreen mode

Output

Simple accordion output

We are keeping it simple in the above example by just toggling the visibility based upon a state variable.


2. Animating the accordion

The component that we created in our previous step was a very simple version of an accordion, which just hides and shows content on the toggle.

But for better UX, we should add animation while toggling the visibility of the content.

  • Using pure CSS to handle animation

This is a good and clean way to handle animations in accordion without any external library dependency

The idea behind this is that in the default state, the max height is set to 0, along with overflow: hidden, so that the content is not visible to the user.

When the accordion is open, we have max-height set to some arbitrarily large value (above 1000px based upon the content of your accordion), so that the content can be seen.

The reason we are using max-height, instead of height, is that we don’t know how tall the container needs to be. By using max-height, we allow the browser to resize the height dynamically when we open it.

The only problem it causes is in Accessibility, when we have multiple accordions and the content includes multiple focusable components then the tab order can be a problem.

Since the tab focus will also go to the component of an accordion which are in a closed state since the accordion component are mounted and are on DOM.

Example:

Accordion problem animation

In the above image, the tab focus gets lost for a second because the focus goes to the button inside the content of the accordion even when it's not in expanded mode.

Ref and more on this here: https://javascript.plainenglish.io/how-to-create-a-simple-animated-accordion-in-react-303567a4ca9f

  • Using react-transition-group's CSSTransition component for handling animation

react-transition-group library provides us an easy way to perform CSS transitions and animations when a React component enters or leaves the DOM.

This fits our case where in we need to mount and unmount content of the accordion-based upon a state variable at the same time also have animation while performing toggle action.

Let code it out!

import React, { useState } from 'react';
import classNames from 'classnames';
import { CSSTransition } from 'react-transition-group';

import ExpandMoreIcon from '@material-ui/icons/ExpandMore';

import './styles.css';

const Accordion = ({ children, isAlreadyOpen, title, id, onOpen, onClose }) => {
  const [isOpen, setIsOpen] = useState(isAlreadyOpen);

  const onToggle = () => {
    if (!isOpen) {
      onOpen(id);
    } else {
      onClose(id);
    }
    setIsOpen((currentState) => !currentState);
  };

  return (
    <section className="accordion-wrapper">
      <div onClick={onToggle} className="accordion-wrapper__header">
        <h4>{title}</h4>
        <span
          className={classNames('accordion-wrapper__header-toggle-icon', {
            'accordion-wrapper__header-toggle-icon--isOpen': isOpen,
          })}
        >
          <ExpandMoreIcon />
        </span>
      </div>
      <div className="accordion-wrapper__content-wrapper">
        <CSSTransition
          in={isOpen}
          timeout={300}
          classNames="accordion-wrapper__content"
          unmountOnExit
        >
          <section>
            <section className="accordion-wrapper__content-body">
              {children}
            </section>
          </section>
        </CSSTransition>
      </div>
    </section>
  );
};

export default Accordion;
Enter fullscreen mode Exit fullscreen mode
.accordion-wrapper {
  background: white;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
  border-radius: 8px;
  border: 2px solid transparent;
  transition: border 0.35s ease;
}

.accordion-wrapper__header {
  display: flex;
  flex: 1;
  cursor: pointer;
  padding: 20px 20px 0px;
  align-items: center;
  justify-content: space-between;
}

.accordion-wrapper__header-toggle-icon {
  background: none;
  border: none;
  display: flex;
}

.accordion-wrapper__header-toggle-icon svg {
  width: 32px;
  height: 32px;
  fill: black;
  transition: all 0.3s linear;
  margin-top: -7px;
}

.accordion-wrapper__header-toggle-icon--isOpen svg {
  transform: rotate(-180deg);
}

.accordion-wrapper__content-wrapper {
  padding: 0px 20px 20px;
}

.accordion-wrapper__content-body {
  padding-top: 24px;
}

/* CSSTransition specific classes starts here */
.accordion-wrapper__content-enter {
  overflow: hidden;
  max-height: 0;
}

.accordion-wrapper__content-enter-active {
  max-height: 1000px;
  transition: max-height 0.6s ease-in-out;
}

.accordion-wrapper__content-exit {
  max-height: 1000px;
}

.accordion-wrapper__content-exit-active {
  overflow: hidden;
  max-height: 0;
  transition: max-height 0.4s cubic-bezier(0, 1, 0, 1);
}

/* CSSTransition specific classes ends here */
Enter fullscreen mode Exit fullscreen mode

In the above code we have used CSSTransition component (more info) for animation, this basically takes a class name and allows us to write the styles when the component will be in different states like enter, enter-active, exit, exit-active and may more states.

Output:

Animated accordion


3. Making Accordion Accessible

The key to making accordions accessible is to toggle some ARIA properties and states on user click or focus events (e.g. aria-hidden, aria-expanded, etc.).

There are majorly two components in Accordion where we can use accessible ARIA properties

  • Accordion Buttons

Buttons are used as the accordions so that they are tab-able by keyboard users and accessible to screen readers.

Each accordion button has a unique id associated with its aria-controls (each button controls this particular id which references the hidden content beneath it).

Here, the aria-controls for each button is: aria-controls='content-{#}'

Each button has an aria-expanded attribute on it that is toggled between true and false.

  • Accordion Content

Every content area has an id that corresponds to the aria-controls for each button.

The content ids are: id='#content-{#}'

Each content area has an aria-hidden attribute that is toggled between true and false.

Let's make our accordion accessible

import React, { useState } from 'react';
import classNames from 'classnames';
import { CSSTransition } from 'react-transition-group';

import ExpandMoreIcon from '@material-ui/icons/ExpandMore';

import './styles.css';

const Accordion = ({ children, isAlreadyOpen, title, id, onOpen, onClose }) => {
  const [isOpen, setIsOpen] = useState(isAlreadyOpen);

  const onToggle = () => {
    if (!isOpen) {
      onOpen(id);
    } else {
      onClose(id);
    }
    setIsOpen((currentState) => !currentState);
  };

  const handleOnKeyPress = (event) => {
    const keys = ['Enter', 'Spacebar', ' '];
    if (keys.includes(event.key)) {
      onToggle();
    }
  };

  return (
    <section className="accordion-wrapper">
      <div
        role="button"
        className="accordion-wrapper__header"
        aria-controls={`${id}-content`}
        aria-expanded={isOpen}
        onClick={onToggle}
        onKeyDown={handleOnKeyPress}
        tabIndex="0"
        aria-labelledby={`${id}-title`}
      >
        <h4 className="accordion-wrapper__header-title" id={`${id}-title`}>
          {title}
        </h4>
        <span
          className={classNames('accordion-wrapper__header-toggle-icon', {
            'accordion-wrapper__header-toggle-icon--isOpen': isOpen,
          })}
        >
          <ExpandMoreIcon />
        </span>
      </div>
      <div
        className="accordion-wrapper__content-wrapper"
        aria-hidden={!isOpen}
        id={`${id}-content`}
      >
        <CSSTransition
          in={isOpen}
          timeout={300}
          classNames="accordion-wrapper__content"
          unmountOnExit
        >
          <section>
            <section className="accordion-wrapper__content-body">
              {children}
            </section>
          </section>
        </CSSTransition>
      </div>
    </section>
  );
};

export default Accordion;
Enter fullscreen mode Exit fullscreen mode

Here we have used role="button" on the accordion header along with onKeyPress to make it accessible, other ARIA attributes like aria-hidden, aria-controls and aria-expanded are also used to convey states of accordion.

More info on accessibility with accordion


Conclusion

Component creation often involves multiple points to be kept in mind, right from creating a basic structure to solving common and complex problems such as accessibility and usability.

The article covers most parts of an Accordion and its uses and can easily be integrated into a live project.

Top comments (0)