Back to blog

React FAQ Sections: 8 Accordion Patterns to Copy-Paste

Published on March 20, 2026·6 min read

FAQ sections reduce support tickets. Every question you answer on the page is a question that doesn't hit your inbox. But a wall of text doesn't work — visitors scan, they don't read. The accordion pattern solves this by hiding answers behind tappable questions, letting users find exactly what they need.

This guide covers 8 FAQ accordion patterns in React with Tailwind CSS, from a minimal implementation to animated, categorized, multi-column layouts.

1. The Native HTML Accordion

Before reaching for JavaScript, consider the built-in <details> and <summary> elements. They're accessible out of the box, require zero state management, and work with JavaScript disabled:

interface FAQItem {
  question: string;
  answer: string;
}

function FAQ({ items }: { items: FAQItem[] }) {
  return (
    <section className="mx-auto max-w-2xl px-6 py-16">
      <h2 className="text-3xl font-bold tracking-tight mb-8">
        Frequently Asked Questions
      </h2>
      <div className="divide-y divide-neutral-200 dark:divide-neutral-800">
        {items.map((item) => (
          <details key={item.question} className="group py-4">
            <summary className="flex cursor-pointer items-center justify-between text-left font-medium">
              {item.question}
              <span className="ml-4 transition-transform group-open:rotate-45">
                +
              </span>
            </summary>
            <p className="mt-3 text-neutral-600 dark:text-neutral-400">
              {item.answer}
            </p>
          </details>
        ))}
      </div>
    </section>
  );
}

The group-open:rotate-45 class rotates the plus sign when the detail is open, turning it into an x shape. Pure CSS, zero JavaScript.

2. Controlled Accordion with useState

When you need only one item open at a time (a common UX pattern), you need state:

"use client";

import { useState } from "react";

function Accordion({ items }: { items: FAQItem[] }) {
  const [openIndex, setOpenIndex] = useState<number | null>(null);

  return (
    <div className="divide-y divide-neutral-200 dark:divide-neutral-800">
      {items.map((item, index) => (
        <div key={item.question} className="py-4">
          <button
            onClick={() => setOpenIndex(openIndex === index ? null : index)}
            className="flex w-full items-center justify-between text-left font-medium"
            aria-expanded={openIndex === index}
          >
            {item.question}
            <ChevronDown
              className={`h-5 w-5 transition-transform ${
                openIndex === index ? "rotate-180" : ""
              }`}
            />
          </button>
          {openIndex === index && (
            <p className="mt-3 text-neutral-600 dark:text-neutral-400">
              {item.answer}
            </p>
          )}
        </div>
      ))}
    </div>
  );
}

The aria-expanded attribute tells assistive technologies whether the content is visible. This is essential for accessibility — don't skip it.

3. Animated Accordion with Framer Motion

The controlled accordion above has a problem: the answer appears and disappears instantly. Adding height animation with Framer Motion makes the interaction feel polished:

import { AnimatePresence, motion } from "framer-motion";

// Inside the map
{openIndex === index && (
  <AnimatePresence>
    <motion.div
      initial={{ height: 0, opacity: 0 }}
      animate={{ height: "auto", opacity: 1 }}
      exit={{ height: 0, opacity: 0 }}
      transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
      className="overflow-hidden"
    >
      <p className="pt-3 text-neutral-600 dark:text-neutral-400">
        {item.answer}
      </p>
    </motion.div>
  </AnimatePresence>
)}

The height: "auto" animation is one of Framer Motion's best features. CSS can't transition to auto height natively — you'd need JavaScript to measure the element first. Framer Motion handles this internally.

4. Multi-Open Accordion

Some FAQ sections work better when multiple items can be open simultaneously. Replace the single openIndex with a Set:

const [openIndexes, setOpenIndexes] = useState<Set<number>>(new Set());

function toggle(index: number) {
  setOpenIndexes((prev) => {
    const next = new Set(prev);
    if (next.has(index)) next.delete(index);
    else next.add(index);
    return next;
  });
}

Use this pattern when the FAQ items are short and users might want to compare answers across questions.

5. Two-Column FAQ

For longer FAQ lists (12+ questions), a single column creates an intimidating scroll. Split the items into two columns with CSS grid:

<div className="grid gap-x-12 gap-y-0 md:grid-cols-2">
  {items.map((item, index) => (
    <AccordionItem key={index} item={item} />
  ))}
</div>

The items flow left to right, top to bottom — item 1 and 2 share the first row, 3 and 4 the second, and so on. This cuts the perceived length of the section in half.

6. FAQ with Categories

Product FAQ pages often span multiple topics: Billing, Features, Security, Account. Group questions under category headings and let users filter:

const categories = ["All", "Billing", "Features", "Security", "Account"];

function CategorizedFAQ({ items }: { items: (FAQItem & { category: string })[] }) {
  const [activeCategory, setActiveCategory] = useState("All");

  const filtered = activeCategory === "All"
    ? items
    : items.filter((item) => item.category === activeCategory);

  return (
    <section>
      <div className="flex gap-2 mb-8">
        {categories.map((cat) => (
          <button
            key={cat}
            onClick={() => setActiveCategory(cat)}
            className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${
              activeCategory === cat
                ? "bg-neutral-900 text-white dark:bg-white dark:text-neutral-900"
                : "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"
            }`}
          >
            {cat}
          </button>
        ))}
      </div>
      <Accordion items={filtered} />
    </section>
  );
}

The pill-style category selector is more inviting than a dropdown. Users can see all categories at a glance and switch instantly.

7. FAQ with Search

For knowledge bases with 50+ questions, add a search input above the accordion. Filter items client-side using a simple includes check on both the question and answer text:

const [query, setQuery] = useState("");

const filtered = items.filter(
  (item) =>
    item.question.toLowerCase().includes(query.toLowerCase()) ||
    item.answer.toLowerCase().includes(query.toLowerCase())
);

Show a "No results found" message when the filtered list is empty. Avoid hiding the search input behind a toggle — if the FAQ is long enough to warrant search, the input should always be visible.

8. FAQ with Schema Markup

Google can display FAQ content directly in search results as rich snippets. Add JSON-LD structured data to your FAQ section:

function FAQSchema({ items }: { items: FAQItem[] }) {
  const schema = {
    "@context": "https://schema.org",
    "@type": "FAQPage",
    mainEntity: items.map((item) => ({
      "@type": "Question",
      name: item.question,
      acceptedAnswer: {
        "@type": "Answer",
        text: item.answer,
      },
    })),
  };

  return (
    <script
      type="application/ld+json"
      // Use a JSON-LD serialization library or sanitize
      // the content before rendering in production
    />
  );
}

Place this component alongside your FAQ section. The structured data doesn't affect visual rendering but tells search engines that your page contains FAQ content eligible for rich results.

Accessibility Checklist

  • Use <button> elements for accordion triggers, not <div> with onClick
  • Include aria-expanded on every trigger
  • Use aria-controls linking the button to the panel's id
  • Ensure keyboard navigation works — Enter and Space should toggle items
  • Maintain visible focus indicators on triggers

Ready-Made FAQ Sections

Building an accessible, animated FAQ from scratch takes time you could spend on your actual product. The Incubator FAQ catalog has 10+ ready-to-use FAQ sections — accordions, two-column, categorized, with search, with schema markup — all built in React and Tailwind CSS.

Explore the full component library for every section your landing page needs, from heroes to pricing.

VA

Victor Aubague

Developer & creator of Incubator

Full-stack developer specialized in React, Next.js, and TypeScript. I built Incubator to help developers ship beautiful interfaces faster — all components are crafted from real client projects and production code.

LinkedIn
React FAQ Sections: 8 Accordion Patterns to Copy-Paste — Incubator