FAQ Accordion with Working Open and Close

A frequently asked questions section with a heading, a short line of text and a contact link on the left, and five questions on the right separated by thin rules. Clicking a question opens its answer and closes the one that was open; clicking it again closes it. The first answer starts open. The plus icon turns into a minus and the open question takes the accent colour. On phones the heading sits above the list.

Code

import { useState } from "react";

const faqs = [
  {
    q: "Can I try Rotaboard before paying?",
    a: "Yes. Every new workspace starts with a 21-day trial that includes all features and up to 50 staff. You only add a card when you decide to keep going, and nothing is charged before the trial ends.",
  },
  {
    q: "How do my staff see their shifts?",
    a: "They get a link by text or email and can open the rota in any browser. There is nothing to install, and changes you publish show up for them straight away.",
  },
  {
    q: "What happens when someone swaps a shift?",
    a: "The two people agree the swap in the app and you get one request to approve. Once approved, the rota, hours and payroll export all update together.",
  },
  {
    q: "Does it work for more than one location?",
    a: "Each location has its own rota and manager, and staff can be shared between them. Owners see hours and wage costs for every location on one screen.",
  },
  {
    q: "Can I export hours for payroll?",
    a: "You can download a spreadsheet for any pay period, with hours split by regular time, overtime and holiday. The column order can be saved to match your payroll provider.",
  },
];

export default function FaqAccordionWithOpenAndClose() {
  const [open, setOpen] = useState<number | null>(0);
  return (
    <section className="bg-white px-6 py-16 text-slate-900 sm:py-24">
      <div className="mx-auto grid max-w-6xl gap-12 lg:grid-cols-3 lg:gap-16">
        <div>
          <p className="text-sm font-semibold text-violet-700">Support</p>
          <h2 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">Questions, answered</h2>
          <p className="mt-4 leading-relaxed text-slate-600">The things managers ask us most before they move their rota over.</p>
          <a href="#" className="mt-6 inline-flex h-11 items-center gap-2 rounded-md bg-violet-50 px-4 text-sm font-semibold text-violet-800 transition hover:bg-violet-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-600 focus-visible:ring-offset-2">
            Talk to our team
            <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" className="h-4 w-4"><path d="M4 10h12M11 5l5 5-5 5" /></svg>
          </a>
        </div>
        <div className="border-t border-slate-200 lg:col-span-2">
          {faqs.map((item, i) => {
            const isOpen = open === i;
            return (
              <div key={item.q} className="border-b border-slate-200">
                <h3>
                  <button
                    type="button"
                    id={`faq-button-${i}`}
                    aria-expanded={isOpen}
                    aria-controls={`faq-panel-${i}`}
                    onClick={() => setOpen(isOpen ? null : i)}
                    className="flex w-full items-center justify-between gap-6 py-5 text-left text-lg font-medium transition hover:text-violet-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-violet-600 focus-visible:ring-offset-2"
                  >
                    <span className={isOpen ? "text-violet-700" : ""}>{item.q}</span>
                    <span aria-hidden="true" className={`flex h-8 w-8 flex-none items-center justify-center rounded-full transition ${isOpen ? "bg-violet-600 text-white" : "bg-slate-100 text-slate-700"}`}>
                      <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="h-4 w-4">
                        <path d="M5 10h10" />
                        <path d="M10 5v10" className={`origin-center transition-transform duration-200 ${isOpen ? "scale-y-0" : ""}`} />
                      </svg>
                    </span>
                  </button>
                </h3>
                <div id={`faq-panel-${i}`} role="region" aria-labelledby={`faq-button-${i}`} hidden={!isOpen}>
                  <p className="max-w-2xl pb-6 pr-10 leading-relaxed text-slate-600">{item.a}</p>
                </div>
              </div>
            );
          })}
        </div>
      </div>
    </section>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/faq-accordion-with-open-and-close.json

When to use it

How to customize it

Accessibility

Questions

How do I animate the answer opening?

Replace the `hidden` attribute with a wrapper that uses `grid transition-all` and switches between `grid-rows-[0fr]` and `grid-rows-[1fr]`, with `overflow-hidden` on the inner element. Add `inert` or `aria-hidden` to closed panels so hidden links cannot be focused.

Can I build an accordion without JavaScript?

Yes, with the native `details` and `summary` elements. You lose the one-open-at-a-time behaviour in older browsers, though the `name` attribute on `details` now gives you that in current ones.

Should only one answer be open at a time?

It keeps the page short and is what this component does. If visitors are likely to compare two answers, let several stay open instead.

Will search engines read answers that are closed?

Yes. The answer text is in the page markup whether the panel is open or not, so it can be indexed. Do not load answers only after a click.

How many questions should an FAQ have?

Five to eight is a good range for a landing page. Past that, group the questions under headings or move them to a help page.

Related: Two-Column FAQ List with No JavaScript, Pricing Table with Monthly and Yearly Toggle, Button Set: Primary, Secondary, Ghost, Icon and Loading, Large Quote Testimonial with Author and Role.

More faq and accordions or all free UI components. Start for free to have a full site built around it.

More in Faq

We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.

Product

Who It's For

Features

Resources

Company

View Sitemap