Create an Animated FAQs Component with styled-components, react-spring, and React Hooks

stoutlabs

Daniel Stout

Posted on August 30, 2019

Create an Animated FAQs Component with styled-components, react-spring, and React Hooks

In this post, we are going to build something I recently created for a client site: an animated FAQs (Frequently Asked Questions) component. It's simple enough to write a tutorial about, while also showing some pretty powerful tools to use in your projects. This is going to be a long post, so let's get right to it!

Here's a quick demo of what we'll make:
FAQs Demo

Note: To follow along with this tutorial, you need a React-based site set up and ready to edit. Based on the topic of this post, I will assume that you don't need help getting to that point. πŸ˜‚ (I just used a Gatsby.js default starter for the demo.)

Install Libraries

To create this FAQs component, we are going to make use of two outstanding React libraries, react-spring and styled-components:

  • react-spring is a powerful and easy to implement animation library built for use within React. We're just barely making use of it in this post, but it's capable of extremely advanced animation sequences. Be sure to check out their docs and examples.

  • styled-components is an amazing CSS-in-JS library that I use with nearly every React project I work on. There are other similar solutions out there, and I have tried most of them more than once... but styled-components continues to be my favorite.

Let's install both of those now:

$ yarn add react-spring styled-components

Afterwards, you will likely need to configure styled-components to work with your React site. For example, in a Gatsby site we would need to install an additional Gatsby plugin, and modify the gatsby-config.js file. I wrote a full post on using styled-components with Gatsby at Alligator.io, if you're interested.

We are also going to make use of React's new Hooks feature, so be sure that you are using React version 16.8.0 or higher. (At the time of writing this post, React is at version 16.9.0.)

Create the Basic Component(s)

Let's first set up a new directory inside our project at /src/components/Faqs/. Inside this directory, let's create two new files:

Faq.js

This file is a React component that functions as an individual FAQ question/answer pair.

/src/components/Faqs/Faq.js

import React, { useState } from "react";

const Faq = props => {
  const { question, answer } = props;
  const [isOpen, toggleOpen] = useState(false);

  return (
    <div onClick={() => toggleOpen(!isOpen)}>
      <div className="faq-question">
        <span>
          Q: {question}
        </span>
      </div>

      <div 
        className="faq-answer" 
        style={isOpen ? { display: "block"} : { display: "none" }}
      >
        <span>
          A: {answer}
        </span>
      </div>
    </div>
  );
};

export default Faq;

As you can see, we are making use of the useState hook in React to track an open/closed state for this component. It doesn't really do much yet, but soon we'll animate the showing & hiding of the answer using react-spring!

FaqsList.js

This file is just a simple React component that will function as a container to hold our list of FAQs:

/src/components/Faqs/FaqsList.js

import React from "react";

import Faq from "./Faq";

// this data could come from anywhere
const faqsData = [
  { 
    question: "What does FAQ stand for?",
    answer: "Frequently Asked Question"
  },
  {
    question: "What is the best ice cream flavor?",
    answer: "Coffee with fudge ripple, or homemade strawberry."
  }
];

const FaqsList = () => {
  return (
    <div>
      {faqsData.map((faq, i) => (
        <Faq key={"faq_" + i} question={faq.question} answer={faq.answer} />
      ))}
    </div>
  );
};

export default FaqsList;

Note the faqsData array of FAQ objects above. This data could come from anywhere (your CMS, an API, etc), but for demo purposes I just hard-coded some data in.

Ok, now that we have our basic components set up... let's add the fun stuff: styles and animation!

Styling With styled-components

Let's create some basic styles for our FaqsList and Faq components. Create a new faq-styles.js file in the same directory as our components, and insert this code:

/src/components/Faqs/faq-styles.js

import styled from "styled-components";

export const StyledFaq = styled.div`
  cursor: pointer;
  margin: 0 0 10px;

  div.faq-question {
    font-size: 125%;
    font-weight: 800;
    margin: 0 0 5px;
  }

  div.faq-answer {
    background: #fff;
    overflow: hidden;

    span {
      display: block; 
      padding: 20px 10px;
    }
  }
`;

export const StyledFaqsList = styled.div`
  background: #efefef;
  margin: 20px 0;
  padding: 1rem;
`;

Notice how we're exporting each of these? This will allow us to import them from the component files we created above. This method will keep all of your FAQs styles all in one location, for easier customizing later on.

Note: This is my typical pattern when making "folder-based" components that I plan to re-use in other locations. A lot of folks seem to think that styles must be within each component file when using CSS-in-JS solutions... but that is incorrect!

Adjust the Components

Let's adjust our Faq.js and FaqsList.js components to make use of these new styles:

/src/components/Faqs/Faq.js

import React, { useState } from "react";

import { StyledFaq } from "./faqStyles";

const Faq = props => {
  const { question, answer } = props;
  const [isOpen, toggleOpen] = useState(false);

  return (
    <StyledFaq onClick={() => toggleOpen(!isOpen)}> 
      <div className="faq-question">
        <span>Q: {question}</span>
      </div>

      <div
        className="faq-answer"
        style={isOpen ? { display: "block" } : { display: "none" }}
      >
        <span>A: {answer}</span>
      </div>
    </StyledFaq> );
};

export default Faq;

All we did above was add an import statement for StyledFaq, and then swap out the outer div element with our imported styled component. Make sense?

Next, we'll do the same thing with the FaqsList component:

/src/components/Faqs/FaqsList.js

import React from "react";

import Faq from "./Faq";
import { StyledFaqsList } from "./faqStyles";

const faqsData = [
  {
    question: "What does FAQ stand for?",
    answer: "Frequently Asked Question!",
  },
  {
    question: "What's the best ice cream flavor?",
    answer: "Coffee with fudge ripple, or homemade strawberry.",
  },
];

const FaqsList = () => {
  return (
    <StyledFaqsList> {faqsData.map((faq, i) => (
        <Faq key={"faq_" + i} question={faq.question} answer={faq.answer} />
      ))}
    </StyledFaqsList> );
};

export default FaqsList;

You should now have a basic styled FAQs list displaying, with each FAQ item showing/hiding the answer when clicked. If yours isn't doing that, I'll post a link to the full source at the end⁠ β€” so don't panic! 😎

Adding Animation With react-spring

Let's add some animation to this with react-spring! To keep it really simple for this post, we will just animate the showing/hiding of the answer portion of each FAQ when clicked.

(And yes, my CSS warrior friends... we could do something like this with pure CSS ⁠— but I want to show usage of react-spring in this post!)

But first, we need to add in a tiny npm package to help us measure the height of our answers. We need that info to tell react-spring what the height is when an answer is in the 'open' state. There's a few available options for this, but I'm going to use react-resize-aware - since it has an easy-to-use hooks-based solution.

Add it to your project, like usual:

$ yarn add react-resize-aware

Now we just need to edit the Faq.js component to add the animations. Below is the updated code:

/src/components/Faqs/Faq.js

import React, { useState } from "react";
import { useSpring, animated } from "react-spring";
import useResizeAware from "react-resize-aware";

import { StyledFaq } from "./faqStyles";

const Faq = props => {
  const { question, answer } = props;
  const [isOpen, toggleOpen] = useState(false);
  const [resizeListener, { height }] = useResizeAware(); 
  const animProps = useSpring({ 
    height: isOpen ? height : 0, 
    opacity: isOpen ? 1 : 0, 
  });

  return (
    <StyledFaq onClick={() => toggleOpen(!isOpen)}>
      <div className="faq-question">
        <span>Q: {question}</span>
      </div>

      <animated.div className="faq-answer" style={{ ...animProps }}> 
        <span style={{ position: "relative" }}> 
          {resizeListener} A: {answer}
        </span>
      </animated.div> </StyledFaq>
  );
};

export default Faq;

To explain a little more, we did the following things above:

  • Imported the two packages we already installed, react-spring and react-resize-aware. We destructured useSpring and animated from react-spring so they're easier to use.
  • Created a new variable for our animation configuration settings, using the useSpring Hook from react-spring. Notice that we set initial values of 0 for the opacity and height, and then our measured height value is used to set the height when the answer is shown. (And of course, opacity is set to 1.)
  • Converted the faq-answer div into a react-spring animated.div element, and spread the values of animProps out into the styles prop.
  • Added a position: relative style to the answer's inner span tag. This is required by react-resize-aware to properly measure the element on load. (See next item.)
  • Added a resizeListener into our answer's inner span. This is part of react-resize-aware, and it measures the answer's height when loaded. (It's essentially an invisible div that returns its width and height via a custom React Hook... so it works perfectly in our stateless component!)

Go ahead and give it a try, if you haven't already. Each FAQ item should now animate open when clicked, and should animate back to closed if clicked again. Pretty cool, huh? You can now re-use this component in any of your sites, and you only need to edit the styles/animations to fit your needs.

Final Thoughts

We are finished! I hope that helps a few of you out there, and maybe even gives you some ideas to try on your own!

Preview/Download Source:

Demo here: https://stoutlabs-faqs-demo.netlify.com/

Demo Source on Github: https://github.com/stoutlabs/demo-spring-hooks.

I am planning to start writing more... so I'll catch you on the next post! πŸ’œ

πŸ’– πŸ’ͺ πŸ™… 🚩
stoutlabs
Daniel Stout

Posted on August 30, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related