2 min read

Tutorial: How to build an Accordion component in React

đź’ˇ
This is a detailed tutorial that solves the Build an Accordion component exercise.
If you haven't already, try to solve it yourself first before reading the solution!

The first step in building the accordion component is to identify the "parts" of the UI, to see if we need to build just one component or more.

Looking at the end result, we notice that the heading + content part is repeated, so this is a great candidate for a component.

Thus, let's create an AccordionHeading component for the header + collapsible content, and another Accordion component as a wrapper for the list.

The Accordion heading

Each heading will receive a title and a content and will display them, allowing the user to click the header to expand/collapse the content.

The content can be passed as a prop, just like the title, but I chose to pass it as children for a nicer component API:

<AccordionHeader title="What is Github and how does it work?">
	GitHub is the home for all developers—a platform where you can share code ...
</AccordionHeader

Here is the final component code:

const AccordionHeader = ({ title, children }) => {
  const [isOpen, setOpen] = useState(false);
  return (
    <section>
      <h3 onClick={() => setOpen(!isOpen)}>{title}</h3>
      {isOpen && <p>{children}</p>}
    </section>
  );
};

The Accordion wrapper

Next, let's build the wrapper component that will display the list. It will simply take the list and iterate through its items to display each as an AccordionHeader.

const Accordion = ({ items }) => (
  <div>
    {items.map((item) => (
      <AccordionHeader title={item.title}>{item.content}</AccordionHeader>
    ))}
  </div>
);

You can check out the full working solution in this CodeSandbox: https://codesandbox.io/p/sandbox/faq-accordion-y85ys9

Bonus question: Only allow one item at a time to be expanded

What if we would only want to allow users to expand one item at a time?
This would mean that the moment on heading is clicked, all the other ones would have to be closed.

To build this, we can no longer track the collapsed/expanded state simply in the AccordionHeading, as we need to know the state of the other headings as well. The solution here is to move the state up - and track which heading is opened in the wrapper Accordion component.

Here is the updated Accordion component:

const Accordion = ({ items }) => {
  const [expandedIndex, setExpandedIndex] = useState(null);

  const handleOnToggle = (index) => {
    if (index === expandedIndex) {
      // If the user clicked the already open header,
      // close it
      setExpandedIndex(null);
    } else {
      setExpandedIndex(index);
    }
  };
  return (
    <div>
      {items.map((item, index) => (
        <AccordionHeader
          title={item.title}
          isOpen={index === expandedIndex}
          onToggle={() => handleOnToggle(index)}
        >
          {item.content}
        </AccordionHeader>
      ))}
    </div>
  );
};

Notice how the AccordionHeader no longer has any state - it gets passed as a prop from the parent component:

const AccordionHeader = ({ title, isOpen, onToggle, children }) => {
  return (
    <section>
      <h3 onClick={onToggle}>{title}</h3>
      {isOpen && <p>{children}</p>}
    </section>
  );
};

You can check out the working code in the same CodeSandbox as above - check the AccordionOneOnly component.