How to Build a fully accessible Accordion with HeadlessUI, Framer Motion, and TailwindCSS
Tags: tailwindcss, framermotion, beginners
Published: 2023-05-04 16:03:02

Before we get started, let's briefly discuss what each of these technologies does:

- **HeadlessUI**: A set of fully accessible, lightweight, unstyled components for building UIs.
- **Framer Motion**: A production-ready motion library for React that makes it easy to add animations and gestures to your UI.
- **TailwindCSS**: A utility-first CSS framework that makes it easy to style your UI using pre-defined classes.

Now, let's dive into how to build an FAQ section with these tools.

## Setup
To get started, you'll need to install the following dependencies to your react project:

```bash
     npm i headlessui framer-motion
```
You can refer to [tailwindcss docs](https://tailwindcss.com/docs/installation) on how to install and configure tailwindcss for the framework of your choice.


## Creating the basic structure for accordion

Create a new tsx or jsx file by the name `Accordion.{tsx/jsx}` in your components folder and paste this block of code.

```tsx
import { Disclosure } from "@headlessui/react";
import { ArrowSmallDownIcon } from "@heroicons/react/24/outline";

const Accordion = ({ question, answer }: FAQ) => {
  return (
    <Disclosure as="li">
      {({ open }) => (
        <>
          <Disclosure.Button className="font-semibold text-xl inline-flex items-center cursor-pointer justify-between w-full mb-1 text-neutral-800">
            {question}{" "}
            <span
              className={`p-2 hover:bg-zinc-400/30 rounded-full ${
                open ? "rotate-180" : ""
              }`}
            >
              <ArrowSmallDownIcon className="w-5 h-5" />
            </span>
          </Disclosure.Button>
          <Disclosure.Panel className="text-sm">{answer}</Disclosure.Panel>
        </>
      )}
    </Disclosure>
  );
};

export default Accordion;

```
We make use of **Disclosure** component provided by headlessui. Our accordion accepts two props : question and answer, both are of string type.


`Disclosure.Button` is the component that toggles our disclosure/ accordion. So we render our question inside this component so that the users can see the answer when they click on the question.

`Disclousre.Panel` is the component that gets mounted and unmounted whenever user clicks on the `Disclosure.Button`.

We use the render prop `open` to conditionally rotate the icon. I am using `ArrowSmallDownIcon` from **Heroicons**. You may use the icon of your choice. You may find more on this topic [here](https://headlessui.com/react/disclosure#using-render-props).

## Adding animations with framer-motion
### for answer

```tsx
import { AnimatePresence } from "framer-motion";

<AnimatePresence>
  <Disclosure.Panel
    as={motion.div}
    initial={{ y: -20, opacity: 0.2 }}
    animate={{ y: 0, opacity: 1 }}
    exit={{
      y: -20,
      opacity: 0.2,
      transition: { duration: 0.2, type: "tween" },
    }}
    transition={{
      duration: 0.15,
      type: "tween",
    }}
    className="text-sm text-neutral-700"
  >
    {answer}
  </Disclosure.Panel>
</AnimatePresence>;
          
```
We must wrap `Disclosure.Panel` component that displays the answer with `AnimatePresence` so that we can add exit animations to it. The `as={motion.div}` prop allows us to use the motion component from Framer Motion and apply animations to the content inside the Disclosure.Panel.

The animation we are going for is pretty straight forward. The answer _slides from top and fades in_ when it is supposed to appear and _slides to the top and fades out_ when it is supposed to disappear. 

## for question
This is quite simple when compared to the former. We just need to toggle the `backgroundColor` of the span that is holding our icon between transparent and an off white color which provides an instant feedback for our users that it is a clickable item.

```tsx
<Disclosure.Button className="font-semibold text-xl inline-flex items-center cursor-pointer justify-between w-full mb-1 text-neutral-800">
  {question}{" "}
  <motion.span
    whileHover={{ backgroundColor: "rgb(161 161 170 / 0.3)" }}
    initial={{ backgroundColor: "transparent" }}
    animate={{ rotate: open ? 180 : 0 }}
    transition={{
      duration: 0.15,
      type: "tween",
    }}
    className="p-2 rounded-full text-neutral-950"
  >
    <ArrowSmallDownIcon className="w-5 h-5" />
  </motion.span>
</Disclosure.Button>;
```
**Note**
You may customize the duration and easing of the animations to your liking.

Accordion component after adding animations

```tsx
import { motion, AnimatePresence } from "framer-motion";
import { Disclosure } from "@headlessui/react";
import { ArrowSmallDownIcon } from "@heroicons/react/24/outline";

const Accordion = ({ question, answer }: FAQ) => {
  return (
    <Disclosure as="li">
      {({ open }) => (
        <>
          <Disclosure.Button className="font-semibold text-xl inline-flex items-center cursor-pointer justify-between w-full mb-1 text-neutral-800">
            {question}{" "}
            <motion.span
              whileHover={{ backgroundColor: "rgb(161 161 170 / 0.3)" }}
              initial={{ backgroundColor: "transparent" }}
              animate={{ rotate: open ? 180 : 0 }}
              transition={{
                duration: 0.15,
                type: "tween",
              }}
              className="p-2 rounded-full text-neutral-950"
            >
              <ArrowSmallDownIcon className="w-5 h-5" />
            </motion.span>
          </Disclosure.Button>
          <AnimatePresence>
            <Disclosure.Panel
              as={motion.div}
              initial={{ y: -20, opacity: 0.2 }}
              animate={{ y: 0, opacity: 1 }}
              exit={{
                y: -20,
                opacity: 0.2,
                transition: { duration: 0.2, type: "tween" },
              }}
              transition={{
                duration: 0.15,
                type: "tween",
              }}
              className="text-sm text-neutral-700"
            >
              {answer}
            </Disclosure.Panel>
          </AnimatePresence>
        </>
      )}
    </Disclosure>
  );
};

export default Accordion;

```

That's it folks !! By following these simple steps you have bult a fully accessible accordion component. You can use this to render an FAQ section. You may find an example on how to use this component below 👇.

```tsx
import Accordion from "./Accordion";

const faqs: FAQ[] = [
  {
    question: "Is vite the best bundler ?",
    answer: `It's difficult to say whether ViteJS is the "best" bundler out there, as it ultimately depends on your specific needs and preferences. ViteJS has gained popularity due to its fast development server and quick build times, which can be beneficial for certain types of projects.`,
  },
  {
    question: "Why should I start using headlessui ?",
    answer:
      "HeadlessUI provides fully accessible, unstyled UI components that are flexible and customizable, can be used with any front-end framework, and are lightweight for optimal performance.",
  },
];

function App() {
  return (
    <main className="min-h-screen bg-fuchsia-400 grid place-items-center font-inter">
      <div className="bg-neutral-50 rounded-xl backdrop-blur-xl w-[70vw] p-12 min-h-[70vh]">
        <ul className="flex flex-col gap-4 mb-8">
          {faqs.map((faq, index) => (
            <Accordion key={index} {...faq} />
          ))}
        </ul>
      </div>
    </main>
  );
}

export default App;

```

You can grab the full source code on [github](https://github.com/bhendi-boi/accordion) and [here](https://accordian-beta.vercel.app/) is a working demo for reference. 


**Happy Hacking**