# FAQ

> FAQ section with a heading, a spring accordion of questions and a "still have questions" contact card whose avatar stack pops in.

- Collection: Blocks
- Tier: free
- Docs: https://geturui.com/docs/faq-section
- Install: `npx shadcn@latest add @geturui/faq-section`
- Dependencies: motion, lucide-react

## Usage

```tsx
import { FaqSection } from "@/components/blocks/faq-section";

export default function Faq() {
  return (
    <FaqSection
      items={[
        { q: "Can I cancel any time?", a: "Yes. Your plan stays active until the end of the billing period." },
        { q: "Do you offer refunds?", a: "Within 14 days, no questions asked." },
      ]}
      contact={{
        title: "Still have questions?",
        description: "Our team replies within a day.",
        actionLabel: "Contact support",
        href: "mailto:support@example.com",
        team: ["Lena Park", "Omar Diaz"],
      }}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| items | `{ q: ReactNode; a: ReactNode; icon?: ReactNode }[]` | — | Questions and answers. |
| defaultOpen | `number` | 0 | Index open on mount; -1 for none. |
| contact | `FaqContact | null` | — | { title, description, actionLabel, href?, onAction?, team?, responseTime? }. null hides the card. |
| layout | `"split" | "stacked"` | "split" | Heading and contact beside the list on wide containers, or everything centred. |
| eyebrow / heading / subheading | `string` | — | Section copy. |

## Accessibility

- Accordion triggers are buttons with aria-expanded and aria-controls; panels are labelled regions.
- The avatar stack is decorative; the contact action is a real link or button.

## Source

### components/blocks/faq-section.tsx

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { MessageCircle } from "lucide-react";
import { cn } from "@/lib/utils";
import { Accordion, type AccordionItem } from "@/components/ui/accordion";
import { buttonClasses, Button } from "@/components/ui/button";

export interface FaqContact {
  title: string;
  description: string;
  actionLabel: string;
  /** Renders the action as a link when set. */
  href?: string;
  onAction?: () => void;
  /** Names for the initials avatar stack. */
  team?: string[];
  /** Small status line, e.g. "Replies in about 2 hours". */
  responseTime?: string;
}

export interface FaqSectionProps {
  eyebrow?: string;
  heading?: string;
  subheading?: string;
  items?: AccordionItem[];
  /** Index open on mount; -1 for none. */
  defaultOpen?: number;
  /** Contact card under the heading. Pass null to hide. */
  contact?: FaqContact | null;
  /** "split" puts heading + contact beside the list on large screens; "stacked" centres everything. */
  layout?: "split" | "stacked";
  className?: string;
}

const DEFAULT_ITEMS: AccordionItem[] = [
  {
    q: "Do I need to install a package?",
    a: "No. The CLI copies the component source into your project, so you own it. The only runtime dependencies are motion and lucide-react.",
  },
  {
    q: "Does it work with my design system?",
    a: "Every component reads CSS variables for surfaces, ink, brand and radii. Map them to your tokens once and the whole library follows.",
  },
  {
    q: "How do Pro blocks get updated?",
    a: "Run the add command again and review the diff. We note breaking changes in the changelog, and older versions stay available.",
  },
  {
    q: "What about reduced motion?",
    a: "Anything bigger than a fade checks the user's reduced-motion preference and falls back to a simple opacity change.",
  },
  {
    q: "Can I use it in client projects?",
    a: "Yes. Free components are MIT licensed, and the Pro licence covers unlimited client projects for your seats.",
  },
];

const DEFAULT_CONTACT: FaqContact = {
  title: "Still have questions?",
  description: "Talk to the people who build GetUrUI. We answer every message ourselves.",
  actionLabel: "Message the team",
  team: ["Nadia Kim", "Theo Laurent", "Ravi Shah"],
  responseTime: "Replies in about 2 hours",
};

function initials(name: string) {
  return name
    .split(/\s+/)
    .slice(0, 2)
    .map((p) => p[0]?.toUpperCase())
    .join("");
}

function ContactCard({ contact }: { contact: FaqContact }) {
  const reduce = useReducedMotion() ?? false;
  const tones = ["bg-volt-soft text-volt-ink", "bg-ember-soft text-ember-ink", "bg-surface-raised text-ink"];
  return (
    <motion.aside
      className="grid gap-4 rounded-lg border border-border bg-surface p-5"
      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 12 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ type: "spring", stiffness: 280, damping: 30, delay: 0.1 }}
    >
      {contact.team && contact.team.length > 0 && (
        <div className="flex items-center gap-3">
          <div className="flex -space-x-2" aria-hidden>
            {contact.team.slice(0, 4).map((n, i) => (
              <motion.span
                key={n}
                className={cn("grid size-9 place-items-center rounded-full text-[12px] font-semibold ring-2 ring-surface", tones[i % tones.length])}
                initial={reduce ? false : { scale: 0.6, opacity: 0 }}
                whileInView={{ scale: 1, opacity: 1 }}
                viewport={{ once: true }}
                transition={{ type: "spring", stiffness: 420, damping: 22, delay: 0.2 + i * 0.07 }}
              >
                {initials(n)}
              </motion.span>
            ))}
          </div>
          {contact.responseTime && (
            <span className="inline-flex items-center gap-1.5 text-[12.5px] font-medium text-ink-muted">
              <span className="size-1.5 rounded-full bg-success text-success animate-pulse-dot" aria-hidden />
              {contact.responseTime}
            </span>
          )}
        </div>
      )}
      <div className="grid gap-1">
        <h3 className="text-[15px] font-semibold text-ink">{contact.title}</h3>
        <p className="text-sm leading-[22px] text-ink-muted">{contact.description}</p>
      </div>
      {contact.href ? (
        <a href={contact.href} onClick={contact.onAction} className={buttonClasses({ variant: "secondary", className: "justify-self-start" })}>
          <MessageCircle className="size-4" aria-hidden />
          {contact.actionLabel}
        </a>
      ) : (
        <Button variant="secondary" className="justify-self-start" onClick={contact.onAction}>
          <MessageCircle className="size-4" aria-hidden />
          {contact.actionLabel}
        </Button>
      )}
    </motion.aside>
  );
}

/** FAQ section: heading, a spring accordion of questions and a "still have questions" contact card with an avatar stack. */
export function FaqSection({
  eyebrow = "FAQ",
  heading = "Questions, answered",
  subheading = "Everything teams usually ask before they add their first component.",
  items = DEFAULT_ITEMS,
  defaultOpen = 0,
  contact = DEFAULT_CONTACT,
  layout = "split",
  className,
}: FaqSectionProps) {
  const split = layout === "split";
  return (
    <section className={cn("@container w-full px-4 py-16 sm:px-6 sm:py-24", className)}>
      <div className={cn("mx-auto grid gap-10", split ? "max-w-6xl @4xl:grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)] @4xl:gap-16" : "max-w-3xl")}>
        <div className={cn("grid content-start gap-6", split ? "@4xl:sticky @4xl:top-24" : "justify-items-center text-center")}>
          <header className="grid gap-3">
            {eyebrow && <span className="text-[13px] font-semibold uppercase tracking-[1px] text-volt-ink">{eyebrow}</span>}
            <h2 className="text-balance font-display text-3xl font-semibold tracking-[-0.8px] text-ink @xl:text-[40px] @xl:leading-[1.1]">{heading}</h2>
            {subheading && <p className="text-balance text-base leading-7 text-ink-muted">{subheading}</p>}
          </header>
          {split && contact && <ContactCard contact={contact} />}
        </div>
        <div className="grid content-start gap-6">
          <Accordion items={items} defaultOpen={defaultOpen} />
          {!split && contact && (
            <div className="text-left">
              <ContactCard contact={contact} />
            </div>
          )}
        </div>
      </div>
    </section>
  );
}

```
