How to create an FAQ accordion using React and Styled Components

Why do this?

Sometimes I want to create a nice, simple component that doesn’t require adding any additional dependencies. Before I made the decision to implement Material UI, I found myself creating a lot of components from scratch. Constantly searching through npm, installing packages, ensuring dependencies are kept up-to-date can cause your codebase to grow out of control.

The benefits of this component are that it’s super lightweight – I already had Styled Components installed, and the icons I pulled from my existing assets. As with most components in React, I wanted this to be reusable, simple and generalisable.

Where to start?

Honestly, sometimes I get lazy, and I let other people do the heavy lifting for me. However, a quick Google search let me down in this case. I couldn’t find what I was looking for anywhere. There were packages, vanilla JavaScript projects, jQuery… The search for how to create a React accordion using styled components was doing me dirty.

So, I thought I’d go back to my vanilla JS days and create one from scratch. This way, I could customise the look and feel to our website, and play with the functionality.

What did I want?

Let’s begin where software engineers always (sometimes? should but often don’t?) begin – the requirements.

  • The accordion shall accept data in the form of a list of objects.
  • The accordion  shall display each question, with a “+” icon if the answer is not showing, and a “-” icon if it is
  • Clicking either the question, or “+” icon shall display the response, and resize the accordion
  • Clicking either the question, or “-” icon shall hide the response, and resize the accordion
  • The accordion shall be mobile-responsive

Having a defined list of requirements meant I was less likely to encounter scope creep, or create something else entirely!

How to do it?

Ok, I know why you’re really here. There’s 3 key parts to this component:

  • Container
  • Question
  • Dropdown

Container

As stated in the requirements, this shall be mobile responsive and resize on display or hide.


const AccordionSection = styled.div`
  width: 100%;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  position: relative;
  transition: height 0.1s ease-in;
  height: 650px;
  ${(props) => {
    if (props.clicked !== false) {
      return `
      height: 900px;
      transition: height 0.1s ease-out;
      `;
    }
  }}
  @media (min-width: 768px) {
    height: 500px;
    ${(props) => {
      if (props.clicked !== false) {
        return `
        height: 700px;
        transition: height 0.1s ease-out;
        `;
      }
    }}
  }
`;

const Container = styled.div`
  margin-top: 2rem;
  position: absolute;
  border-top: 1px solid rgb(175, 175, 175);
  overflow: hidden;
  width: 95%;
  @media (min-width: 992px) {
    width: ${maxWidth};
    max-width: ${maxWidth};
  }
`;

What’s going on under the hood here? The section itself will span the entire width of the page, and display the accordion in the centre. The transition property let’s the accordion  grow or shrink in a smooth way that adds to user experience. The section height will change based on whether a dropdown is displayed, and if it is displayed on mobile.

The container simply adds some styling, and sets a maximum width based on a global variable maxWidth.

Questions


const Wrap = styled.div`
  background: #ffffff;
  color: #000a33;
  border-bottom: 1px solid rgb(175, 175, 175);
  display: flex;
  justify-content: space-between;
  align-items: center;
  width: 100%;
  text-align: left;
  cursor: pointer;
  h1 {
    padding: 1.8rem;
    font-size: 1.2rem;
    color: #000a33;
    margin-bottom: 0;
  }
  span {
    margin-right: 1.5rem;
  }
`;

I wanted a simple aesthetic, so kept the background white, with dark grey writing. Each is separated by a thin border, and I’ve added some simple styling to the text. The icon displayed changes based on whether the index of the question in question (see what I did there?) is clicked, if so, display a “-”icon, otherwise it’s the standard “+”.

Given the requirement of reading data, I chose to map over this data and return each question. Each question has an onClick handler, that sets state to display that dropdown. Not forgetting some checks for whether a question is already clicked!

Dropdown


const Dropdown = styled.div`
  background: #fff;
  color: #00ffb9;
  width: 100%;
  height: auto;
  min-height: 120px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: left;
  padding: 0 2rem;
  border-bottom: 1px solid #aeaeb1;
  border-top: 1px solid #aeaeb1;
  p {
    font-size: 1.2rem;
    padding: 1rem 0;
  }
`;

This is pretty simple – this is only displayed if clicked is true. Since this is handled using JS, this definitely does not optimise for SEO .Should you wish to do so, it’s better to handle displaying the dropdown using CSS so it isn’t unmounted each time it’s clicked.

In this case, the dropdown component is displayed with the text and any contact information from the data. This allowed me to add in contact information using a tags, instead of plain text.

Bringing It All Together

Related Entries