Skip to content
goodtabs UI
BrowseBrowse components

FAQ tabs

Questions grouped behind topic tabs, each answer a native disclosure.

npx shadcn@latest add https://ui.goodtabs.co/r/faq-tabs.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/faq-tabs

When am I charged?
On the day you subscribe, then every month or year after.
Do you send invoices?
Yes, by email after every payment, with your company details.

Billing, account and usage, with a short list each.

Two topics

Can I change the theme?
Everything reads from shadcn tokens, so edit the variables in your CSS.
Is dark mode included?
Yes. Light and dark are full mirrors of each other.

A different topic open first.

Usage

import { FaqTabs } from "@/components/faq-tabs"

<FaqTabs topics={[{ value: "billing", label: "Billing", items: [{ question: "When am I charged?", answer: "On signup." }] }]} />

Props

PropTypeDefaultDescription
topicsFaqTopic[]value, label and items of question and answer.
labelstring"Topics"Accessible name for the tab row.
defaultValuestringTopic open first. Defaults to the first.

Source

faq-tabs/faq-tabs.tsx
"use client";

import * as React from "react";
import { ChevronDownIcon } from "lucide-react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn } from "@/lib/utils";

type FaqItem = { question: string; answer: React.ReactNode };

type FaqTopic = {
  /** Unique id for the topic tab. */
  value: string;
  /** The tab label, for example "Billing". */
  label: string;
  /** Questions in this topic. */
  items: FaqItem[];
};

type FaqTabsProps = Omit<React.ComponentProps<typeof Tabs>, "children"> & {
  /** Topics, one per tab. */
  topics: FaqTopic[];
  /** Accessible name for the tab row. */
  label?: string;
};

/**
 * Questions grouped behind topic tabs. Each panel is a list of native details and summary pairs,
 * with one open at a time per topic. Client component because radix tabs hold the active topic.
 */
export function FaqTabs({ topics, label = "Topics", defaultValue, className, ...props }: FaqTabsProps) {
  return (
    <Tabs
      data-slot="faq-tabs"
      defaultValue={defaultValue ?? topics[0]?.value}
      className={cn("w-full gap-4", className)}
      {...props}
    >
      <TabsList variant="line" aria-label={label} className="w-full justify-start overflow-x-auto border-b pb-1">
        {topics.map((t) => (
          <TabsTrigger
            key={t.value}
            value={t.value}
            className="h-7 flex-none px-2.5 text-foreground/70 dark:text-foreground/70"
          >
            {t.label}
          </TabsTrigger>
        ))}
      </TabsList>
      {topics.map((t) => (
        <TabsContent key={t.value} value={t.value} className="divide-y rounded-lg border bg-card">
          {t.items.map((item) => (
            <details key={item.question} name={`faq-${t.value}`} className="group/faq">
              <summary className="flex min-h-11 cursor-pointer list-none items-center justify-between gap-4 px-4 py-3 text-sm font-medium text-foreground outline-none select-none hover:bg-accent/40 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:ring-inset [&::-webkit-details-marker]:hidden">
                {item.question}
                <ChevronDownIcon
                  className="size-4 shrink-0 text-muted-foreground transition-transform duration-150 group-open/faq:rotate-180 motion-reduce:transition-none"
                  aria-hidden="true"
                />
              </summary>
              <div className="px-4 pb-4 text-sm text-muted-foreground">{item.answer}</div>
            </details>
          ))}
        </TabsContent>
      ))}
    </Tabs>
  );
}

export type { FaqTopic, FaqItem };

For AI agents

Hand your agent this page, the markdown at /components/faq-tabs/prompt.md, or the Copy AI prompt button above.

When to use

  • A FAQ with more than about eight questions that fall into clear topics
  • On pricing and support pages
  • When readers know which topic they need

When not to use

  • For a short list, use faq-accordion
  • For documentation, which needs search and headings
  • For questions that do not group

Agent notes

  • Client component for the tabs; answers are native details elements, one open at a time per topic
  • Keep topic labels to one word, they scroll sideways on phones
  • Answer takes a string or any element, such as a link
  • Inactive topics are unmounted, so find in page only sees the open one