FAQ that answers real objections
Answers the questions buyers ask before committing, with direct answers and a real contact route, using native disclosure widgets.
Community — not yet reviewed by a maintainer
Preview
1440 px wide. Open this preview on its own ↗
Install
npx shadcn@latest add online-scope-lab/tessli/faqRun it in a project set up with shadcn/ui. It adds faq.tsx to your components folder, and the item takes on your project’s theme.
When to use it
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.
Common AI default vs. this
The common 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.
What this item does instead
- 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.
Why it is built this way
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.
Rules
- H-02
- S-02
- K-01
- W-04
- R-03
- M-01
- M-02
- A-06
Each rule page says why the rule matters and how to check it. Rules are drafts until a maintainer signs them; see all rules.
How it breaks
Common mistakes when adapting this item.
- 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, responsive behaviour and accessibility
- States
- default
- 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.
Code
web/registry/items/faq/faq.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>
);
}
Credits and licence
- Contributors
- None credited yet.
- Provenance
- Original work
- Licence
- MIT
- Review
- Community — not yet reviewed by a maintainer
For agents
The same taste notes and code as structured data.