# FAQ

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

A shadcn/ui block from the [@geturui](https://geturui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/geturui/faq-section).

- Page, with a live preview: https://shadcn.exchange/r/geturui/faq-section
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/geturui/faq-section-light-2bc02060917ad202.png
- Registry item: https://geturui.com/r/faq-section.json
- Categories: blocks, faq, accordion, questions, support, contact
- Listed: 2026-10-23, source last changed 2026-10-23

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/geturui/faq-section.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`, `motion`
- Registry items: `@geturui/accordion`, `@geturui/button`

## Files

### `registry/geturui/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>
  );
}
```

### `LICENSE-geturui.md`

```md
MIT License

Copyright (c) 2026 geturui.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
