# Tessli library item — FAQ that answers real objections

Answers the questions buyers ask before committing, with direct answers and a real contact route, using native disclosure widgets.

Kind: section
Category: faq
Status: community (not yet reviewed by a maintainer)
Tessli page: /library/faq
Preview: /preview/faq

## Install

```sh
npx shadcn@latest add online-scope-lab/tessli/faq
```

## Use when

- A marketing or pricing page has four to eight recurring questions about data, billing, cancellation or access.
- The answers are short and stable enough to live on the page.

## Avoid when

- The questions need long, step-by-step answers; link to documentation instead.
- You have fewer than three real questions; answer them in the page copy.
- The content is support troubleshooting that changes often; keep it in a help centre.

## Rules

- H-02 — One h1 per page; nest h2–h4 in order. Level follows structure, size follows style. (/rules/H-02)
- S-02 — Align a section's heading, text and actions on one left edge; centre only short blocks. (/rules/S-02)
- K-01 — Use the native element (button, a, details, input, select, dialog) before a custom widget. (/rules/K-01)
- W-04 — Lead answers, errors and notices with the answer or consequence, then the detail. (/rules/W-04)
- R-03 — Change layout where the content stops fitting, not at device names. (/rules/R-03)
- M-01 — Animate only changes people cause (open, close, switch); no decorative motion. (/rules/M-01)
- M-02 — Under prefers-reduced-motion, remove non-essential transforms and transitions. (/rules/M-02)
- A-06 — Make everything work by keyboard, close overlays on Escape, and hide nothing behind hover. (/rules/A-06)

## Common AI default

A centred "Frequently Asked Questions" heading over a JavaScript accordion of filler questions such as "What is [product]?" and "Why choose us?", answered with marketing copy, only one item open at a time, and no way to reach a person.

## Why this is better

- Questions are real objections (export, cancellation, billing, sharing, offline) in the words a buyer would use.
- Each answer starts with the direct answer ("Yes.", "Only the people you invite.") before the detail.
- Native details and summary elements need no JavaScript, work with the keyboard and screen readers out of the box, and let people open several answers at once.
- A heading that says what the questions are about, plus a real contact route for anything not covered.
- On wide screens the heading column stays in view beside the questions instead of a centred single column.

## Common failures when adapting it

- Padding the list with questions nobody asks to make the section look complete.
- Hiding prices, limits or cancellation terms in an FAQ instead of stating them where people decide.
- Replacing the native disclosure with a custom accordion that loses keyboard and find-in-page support.
- Linking the contact line to a form that does not exist yet.

## States

- default

## Responsive behaviour

- Desktop: Two columns from 1024px; the heading, description and contact route stay in view beside the questions.
- Mobile: One column; the heading block comes first, then full-width questions with 44px touch targets.

## Accessibility

- The section is labelled by its h2; questions are summary elements inside details, so their expanded state is announced natively.
- The plus icon is decorative (aria-hidden) and rotates into a cross; the open state never relies on the icon alone.
- Each summary is at least 44px tall and shows a focus ring.
- The rotation honours reduced motion.

## Notes

An FAQ earns its place when it removes a reason not to buy. That means real objections: can I leave with my data, what happens if I stop paying, who can see my work. Filler questions about what the product is belong in the hero, not here.

The section uses native `details` and `summary` elements. They are keyboard- and screen-reader-accessible without any script, and people can open more than one answer to compare them. Keep answers to a few sentences and lead with the direct answer.

The contact line is part of the item, not decoration: if the answer is not on the page, say who answers and how fast, and only if that is true.

## Credits

- Contributors: none credited yet
- Provenance: original
- Licence: MIT

## Source

`web/registry/items/faq/faq.tsx`

```tsx
import * as React from "react";

import { cn } from "@/registry/shadcn/lib/utils";

export type FaqItem = {
  /** The question as a buyer would ask it, not a marketing prompt. */
  question: string;
  /** A direct answer first, then the detail. Keep it to a few sentences. */
  answer: React.ReactNode;
};

export type FaqProps = Omit<React.ComponentProps<"section">, "title"> & {
  title: React.ReactNode;
  /** One sentence on what these questions cover. */
  description?: React.ReactNode;
  items: FaqItem[];
  /** Where to go when the answer is not here: a real contact route. */
  contact?: React.ReactNode;
  /** Open the first answer on load. Off by default. */
  openFirst?: boolean;
};

export function Faq({
  title,
  description,
  items,
  contact,
  openFirst = false,
  className,
  ...props
}: FaqProps) {
  const headingId = React.useId();

  return (
    <section
      aria-labelledby={headingId}
      data-slot="faq"
      className={cn("bg-background text-foreground", className)}
      {...props}
    >
      <div className="mx-auto grid w-full max-w-6xl gap-8 px-4 py-12 sm:px-6 sm:py-16 lg:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] lg:gap-16 lg:px-8 lg:py-24">
        <div className="lg:sticky lg:top-8 lg:self-start">
          <h2
            id={headingId}
            className="text-3xl leading-tight font-semibold tracking-tight text-balance sm:text-4xl"
          >
            {title}
          </h2>
          {description ? (
            <p className="mt-4 max-w-prose text-base leading-relaxed text-pretty text-muted-foreground">
              {description}
            </p>
          ) : null}
          {contact ? (
            <div className="mt-6 max-w-prose text-sm leading-relaxed text-muted-foreground">
              {contact}
            </div>
          ) : null}
        </div>

        <div className="border-t border-border">
          {items.map((item, index) => (
            <details
              key={item.question}
              open={openFirst && index === 0}
              className="group border-b border-border"
            >
              <summary className="flex min-h-11 cursor-pointer list-none items-start justify-between gap-4 rounded-sm py-4 text-left text-base leading-snug font-medium focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none [&::-webkit-details-marker]:hidden">
                <span>{item.question}</span>
                <svg
                  aria-hidden="true"
                  viewBox="0 0 16 16"
                  className="mt-1 size-4 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45 motion-reduce:transition-none"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth="1.5"
                  strokeLinecap="round"
                >
                  <path d="M8 3v10M3 8h10" />
                </svg>
              </summary>
              <div className="max-w-prose pr-8 pb-5 text-sm leading-relaxed text-pretty text-muted-foreground">
                {item.answer}
              </div>
            </details>
          ))}
        </div>
      </div>
    </section>
  );
}
```

## Interpretation boundaries

- Community items are merged and pass automated checks (schema, build, accessibility, overflow and console errors in three themes at five widths); only items with status reviewed have been checked by a maintainer.
- Each rule id links to its page at /rules/<id>; every rule is a draft until a maintainer signs it.
- Tessli does not claim that using an item improves AI-generated UI; that claim waits for a published evaluation.
