FAQ Accordion

Accessible accordion with smooth height animation and rotating icon.

Frequently asked questions

Yes. Everything is MIT licensed — use it in personal, client and commercial work without attribution.

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/faq-accordion.tsx

    components/ui/faq-accordion.tsx
    "use client";
    
    import { AnimatePresence, motion } from "motion/react";
    import { Plus } from "lucide-react";
    import { useState } from "react";
    
    type FaqItem = { question: string; answer: string };
    
    const faqs: FaqItem[] = [
      { question: "Can I use the components in commercial projects?", answer: "Yes. Everything is MIT licensed — use it in personal, client and commercial work without attribution." },
      { question: "Which frameworks are supported?", answer: "Components are written in React with TypeScript and Tailwind CSS. They work in Next.js, Vite, Remix and Astro." },
      { question: "Do I need to install a package?", answer: "No. Copy the source into your project and own it. The only dependencies are motion and lucide-react." },
      { question: "Are the components accessible?", answer: "We use semantic HTML, visible focus states and respect reduced-motion settings wherever animation matters." },
    ];
    
    export function FaqAccordion({ items = faqs }: { items?: FaqItem[] }) {
      const [open, setOpen] = useState<number | null>(0);
    
      return (
        <div className="divide-y divide-zinc-200 rounded-2xl border border-zinc-200 bg-white">
          {items.map((item, i) => {
            const isOpen = open === i;
            return (
              <div key={item.question}>
                <button
                  onClick={() => setOpen(isOpen ? null : i)}
                  aria-expanded={isOpen}
                  className="flex w-full items-center justify-between gap-6 px-6 py-5 text-left font-medium text-zinc-900 transition-colors hover:bg-zinc-50/70"
                >
                  {item.question}
                  <motion.span
                    animate={{ rotate: isOpen ? 45 : 0 }}
                    transition={{ type: "spring", stiffness: 300, damping: 22 }}
                    className={`grid size-7 shrink-0 place-items-center rounded-full border transition-colors ${isOpen ? "border-zinc-900 bg-zinc-900 text-white" : "border-zinc-200 text-zinc-500"}`}
                  >
                    <Plus className="size-4" />
                  </motion.span>
                </button>
                <AnimatePresence initial={false}>
                  {isOpen && (
                    <motion.div
                      initial={{ height: 0, opacity: 0 }}
                      animate={{ height: "auto", opacity: 1 }}
                      exit={{ height: 0, opacity: 0 }}
                      transition={{ duration: 0.35, ease: [0.22, 1, 0.36, 1] }}
                      className="overflow-hidden"
                    >
                      <p className="px-6 pb-5 text-sm leading-relaxed text-zinc-500">{item.answer}</p>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            );
          })}
        </div>
      );
    }
    
    export default function Demo() {
      return (
        <div className="w-full max-w-2xl">
          <h2 className="mb-8 text-center text-3xl font-semibold tracking-tight text-zinc-900">Frequently asked questions</h2>
          <FaqAccordion />
        </div>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.