Skip to content

FAQ Categories

Help-center style FAQ: topic buttons with counts on the left (stacked on desktop, wrapped on mobile), and a ruled accordion on the right whose open question floods yellow.

brutalist/section/faq-categories
Open ↗

Source

"use client";

import { useId, useState } from "react";
import { Accordion } from "radix-ui";
import { cn } from "@/lib/utils";

export interface FaqCategoriesGroup {
  category: string;
  items: { question: string; answer: string }[];
}

export interface FaqCategoriesProps {
  kicker?: string;
  title?: string;
  groups?: FaqCategoriesGroup[];
  className?: string;
}

const DEFAULT_GROUPS: FaqCategoriesGroup[] = [
  {
    category: "Product",
    items: [
      { question: "What counts as a release?", answer: "Anything you publish: a version, a hotfix or a simple announcement. Releases are unlimited on every plan." },
      { question: "Can I edit a release after publishing?", answer: "Yes. Edits sync to the page, widget and RSS. Email digests already sent stay as they were." },
      { question: "Does the widget work with SPAs?", answer: "Yes. It's framework-agnostic and ships React, Vue and Svelte bindings." },
    ],
  },
  {
    category: "Billing",
    items: [
      { question: "Do you charge per seat?", answer: "No. You pay per project. Invite your whole company." },
      { question: "Do you offer discounts?", answer: "Free for open source, 50% for non-profits and education, 20% off with yearly billing." },
    ],
  },
  {
    category: "Security",
    items: [
      { question: "What do you read from my repo?", answer: "PR titles, labels, descriptions and merge dates. Never your source code." },
      { question: "Are you SOC 2 compliant?", answer: "Yes, SOC 2 Type II. The report is available under NDA on the Scale plan." },
    ],
  },
];

export function FaqCategories({ kicker = "[ 07 ] Help center", title = "Pick a topic.", groups = DEFAULT_GROUPS, className }: FaqCategoriesProps) {
  const [active, setActive] = useState(0);
  const id = useId();
  const group = groups[active] ?? groups[0];
  if (!group) return null;

  return (
    <section className={cn("da-section da-stroke-b bg-da-bg text-da-fg", className)} aria-labelledby={`${id}-title`}>
      <div className="da-container grid gap-10 lg:grid-cols-[280px_1fr]">
        <div>
          <p className="font-da-mono text-xs font-bold tracking-da-label text-da-muted-fg uppercase">{kicker}</p>
          <h2 id={`${id}-title`} className="mt-3 font-da-display text-[clamp(2rem,5vw,3.5rem)] leading-[0.95] tracking-da-display uppercase">
            {title}
          </h2>
          <div role="group" aria-label="FAQ topics" className="mt-8 flex flex-wrap gap-3 lg:flex-col">
            {groups.map((g, i) => (
              <button
                key={g.category}
                type="button"
                aria-pressed={i === active}
                onClick={() => setActive(i)}
                className={cn(
                  "da-focus da-transition da-stroke flex items-center justify-between gap-6 px-4 py-3 text-left font-bold",
                  i === active ? "bg-da-fg text-da-bg shadow-da-sm" : "bg-da-surface text-da-surface-fg hover:bg-da-primary hover:text-da-primary-fg",
                )}
              >
                {g.category}
                <span className="font-da-mono text-xs">{g.items.length}</span>
              </button>
            ))}
          </div>
        </div>

        <div aria-live="polite">
          <h3 className="sr-only">{group.category}</h3>
          <Accordion.Root key={group.category} type="single" collapsible defaultValue="0" className="da-stroke bg-da-surface text-da-surface-fg shadow-da-lg">
            {group.items.map((item, i) => (
              <Accordion.Item key={item.question} value={String(i)} className={cn(i > 0 && "da-stroke-t")}>
                <Accordion.Header asChild>
                  <h4>
                    <Accordion.Trigger className="da-focus group flex w-full items-center justify-between gap-4 p-5 text-left text-lg font-bold data-[state=open]:bg-da-primary data-[state=open]:text-da-primary-fg">
                      {item.question}
                      <span aria-hidden className="font-da-mono text-2xl leading-none group-data-[state=open]:hidden">
                        +
                      </span>
                      <span aria-hidden className="hidden font-da-mono text-2xl leading-none group-data-[state=open]:inline">
                        −
                      </span>
                    </Accordion.Trigger>
                  </h4>
                </Accordion.Header>
                <Accordion.Content className="da-stroke-t overflow-hidden px-5 py-4 text-da-muted-fg">{item.answer}</Accordion.Content>
              </Accordion.Item>
            ))}
          </Accordion.Root>
        </div>
      </div>
    </section>
  );
}

export default FaqCategories;

modules/brutalist/section/faq-categories/index.tsx

Props

PropTypeDefaultDescription
kicker / titlestring—Header copy.
groupsFaqCategoriesGroup[]—{ category, items: { question, answer }[] }[].
classNamestring—Classes on the <section>.

Other faq variants in Brutalist

FAQ in other art directions

Faq Cards

All-open FAQ: a 3-column grid of glass cards, each with a gradient number badge, question and answer, followed by a glass contact pill with an ink button.

GlassFAQ

Faq Chat

FAQ as a chat: suggested question chips on the left; picking one adds a gradient user bubble and, after a typing indicator, a glass answer bubble in the conversation panel.

GlassFAQ

FAQ Glass

Centered FAQ with stacked frosted accordion cards; the plus button spins into a gradient cross with a slight overshoot and answers expand smoothly. Built on Radix Accordion.

GlassFAQ

Faq Search

Help-center FAQ: large glass search pill filtering a frosted Radix accordion live, with category tags, result count and highlighted matches; empty state.

GlassFAQ

Faq Grid

All answers visible at once: a three-column grid of short questions and answers under a header, followed by two support link cards (documentation, talk to us).

MinimalFAQ

Faq List

Centered single-column FAQ built on native details/summary (zero JS): hairline dividers, a plus icon that rotates into a cross when open, and a contact line underneath.

MinimalFAQ

FAQ Split

Two-column FAQ: title, description and support link on the left, hairline-divided Radix accordion on the right with a rotating chevron and a smooth height + fade expand.

MinimalFAQ

Faq Tabs

FAQ grouped by category behind an underline tab bar (Radix Tabs, with counts); the active category shows every question and answer in a two-column definition list.

MinimalFAQ

Faq Inline

Ultra-compact FAQ: short questions and one-line answers flowing as a 3-column grid of mono question labels over large answers — scannable in seconds.

Mono CleanFAQ

Faq List

Numbered accordion FAQ in a ruled list: mono index + question, a +/− glyph that swaps, answer indented under the question; left column holds the label and title.

Mono CleanFAQ

Faq Split

Always-open FAQ as a two-column definition list (question left in medium weight, answer right in muted text) on hairline rows, followed by a mono contact line.

Mono CleanFAQ

Faq Tabs

FAQ grouped by topic with text tabs (Radix Tabs) underlined in ink when active, and each topic’s answers as a ruled definition list.

Mono CleanFAQ

Faq Accordion

Two-column FAQ: heading + description + contact link on the left, bordered Radix accordion with plus icons rotating to × on the right.

Neo CorporateFAQ

Faq Security

Security & compliance FAQ: dark-tinted certification cards (SOC 2, ISO 27001, GDPR, PCI) with shield icons, then security questions as native <details> disclosure rows.

Neo CorporateFAQ

Faq Sidebar

Help-center style FAQ: a vertical category nav (left, becomes horizontal scroll chips on mobile) and the selected category’s questions as an open definition list.

Neo CorporateFAQ

Faq Support

Compact 2×3 grid of short Q&As followed by three bordered support channel cards (docs, chat, dedicated support).

Neo CorporateFAQ

Faq Columns

Editorial FAQ: serif title and intro in a narrow left column, questions as a two-column definition list on the right separated by warm hairlines.

Organic SoftFAQ

Faq Contact

FAQ beside a contact card: native disclosure list on the left, and a clay-tinted card with a sprouting-leaf illustration, email and chat buttons on the right.

Organic SoftFAQ

Faq Search

Searchable FAQ: rounded search field plus topic pills filter a list of questions (native details), with a friendly empty state and live result count.

Organic SoftFAQ

Faq Soft

Soft accordion: each question is its own rounded paper pill-card that turns sage when open, with a round plus that rotates into a cross; generous spacing.

Organic SoftFAQ

Faq Columns

All-visible FAQ: sticky intro with a peach “Chat with us” pill on the left, six Q&As in a 2-column grid with round lavender number badges on the right.

Soft FlatFAQ

Faq Help

Help-center FAQ: lavender panel with a friendly greeting, big white search pill and popular-search chips; matching answers show as white cards with a peach empty state.

Soft FlatFAQ

Faq Pills

Single-column FAQ of rounded white blocks on native details/summary; the open item turns lavender and its round chevron button flips and fills periwinkle.

Soft FlatFAQ

Faq Tinted

FAQ by topic: three big pastel topic buttons (emoji + label) switch the list of white Q&A cards below, which fades in on change.

Soft FlatFAQ