Logo

dev-resources.site

for different kinds of informations.

How to Build a fully accessible Accordion with HeadlessUI, Framer Motion, and TailwindCSS

Published at
5/4/2023
Categories
tailwindcss
framermotion
beginners
Author
bhendi
Categories
3 categories in total
tailwindcss
open
framermotion
open
beginners
open
Author
6 person written this
bhendi
open
How to Build a fully accessible Accordion with HeadlessUI, Framer Motion, and TailwindCSS

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:

     npm i headlessui framer-motion
Enter fullscreen mode Exit fullscreen mode

You can refer to tailwindcss docs 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.

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;

Enter fullscreen mode Exit fullscreen mode

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.

Adding animations with framer-motion

for answer

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>;

Enter fullscreen mode Exit fullscreen mode

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.

<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>;
Enter fullscreen mode Exit fullscreen mode

Note
You may customize the duration and easing of the animations to your liking.

Accordion component after adding animations

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;

Enter fullscreen mode Exit fullscreen mode

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 ๐Ÿ‘‡.

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;

Enter fullscreen mode Exit fullscreen mode

You can grab the full source code on github and here is a working demo for reference.

Happy Hacking

framermotion Article's
30 articles in total
Favicon
Building a food simulation game with Next.js and generative design
Favicon
How to Create a Flipping Card Animation Using Framer Motion
Favicon
State-of-the-Art Web Design with Remix, Tailwind, and Framer Motion
Favicon
How I Created a Stunning Portfolio with Next.js, Tailwind CSS, and Framer Motion
Favicon
Creating the iMessage Card Stack Animation: The Interactive Layer
Favicon
Creating the iMessage Card Stack Animation: The Timelineย Design
Favicon
How to build awesome website with stunning animation?
Favicon
Adding motion to 3D models with Framer Motion and Three.js
Favicon
Creating a Smooth Animated Menu with React and Framer Motion
Favicon
My-Portfolio
Favicon
Watch Out For Broken Links, 404 Page With Framer Motion, TailwindCSS and NextJs
Favicon
Could not find a declaration file for module framer-motion Error in Next.js 14
Favicon
How to add Animations and Transitions in React
Favicon
Creating a text writing animation in React using Framer-Motion
Favicon
Unveiling My Portfolio Website: A Fusion of Tech and Creativity ๐Ÿš€
Favicon
React developer portfolio template use framer motion to animate components
Favicon
Building a mini casual game with Next.js
Favicon
Creating a Multi-Level Sidebar Animation with Tailwind and Framer Motion in React
Favicon
Easy Animated Tabs in ReactJS with Framer Motion
Favicon
A Tinder-like card game with Framer-Motion
Favicon
Elevating Web Design with Framer Motion: Bringing Your Website to Life
Favicon
Animating the Web: Route Transitions in Next.js with Framer Motion
Favicon
Enhancing Form Usability with Framer Motion: A Guide to Animated, Chunked Form Transitions
Favicon
How to Build a fully accessible Accordion with HeadlessUI, Framer Motion, and TailwindCSS
Favicon
Build Portfolio Theme in Tailwind CSS & Next.js
Favicon
How to create Scroll-Linked Animations with React and Framer Motion ๐Ÿ’ƒ๐Ÿป๐Ÿ•บ๐Ÿป
Favicon
How to build 3 simple animation with framer motion
Favicon
How to create an awesome navigation menu using chakra-UI and framer-motion.
Favicon
Make a slide open envelope
Favicon
Animate everything with Framer Motion

Featured ones: