FAQ
StableQuestions and answers on native <details>: one answer open at a time, with no JavaScript required.
Every row is a <details> element and they all share a name, so opening one closes the one that was open. Answers ease open where the browser can animate it, the plus turns into a minus, and fading hairlines sit between rows. Pass your own questions with items; without it you get the SAMPLE questions for the home page concepts from home/content.ts.
Questions about this Library
import { Faq } from '@rchursin/home/Faq'
const questions = [ { q: 'Is the code on these pages the code that runs?', a: 'Yes. Each example’s code view is the example file itself, read as text by the bundler, so the preview and the code can’t drift apart.', }, { q: 'Does this accordion need JavaScript?', a: 'No. It is the browser’s own <details> element, so it opens, closes and works with a keyboard and a screen reader before any script loads.', }, { q: 'Can more than one answer be open at once?', a: 'Not in current browsers. The answers share a group name, so opening one closes the other. Older browsers ignore the group and let several stay open.', }, { q: 'What is the Library built with?', a: 'React 19, TypeScript and Tailwind CSS v4. React and React DOM are its only runtime dependencies.', },]
/** Pass your own questions with `items`. Leave it out and you get the sample questions from home/content.ts. */export default function FaqLibrary() { return <Faq items={questions} className="w-full max-w-2xl" />}Usage
import { Faq } from '@rchursin/home/Faq'<Faq items={[{ q: 'Does it work offline?', a: 'Yes. Everything is saved in your browser.' }]} />API
FAQ props
| Prop | Type | Default | Description |
|---|---|---|---|
items | typeof faqs | faqs | The questions, as { q: string; a: string } objects. The default is the sample list in home/content.ts, written for the home page concepts. |
className | string | – | Extra classes on the list. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves from question to question. |
| EnterSpace | Opens or closes the focused question. Opening one closes whichever was open. |
- Each question is the <summary> of a <details>, so browsers expose it as a control with its expanded or collapsed state. No ARIA is needed.
- The rows share one name (from useId), which makes the group exclusive in current browsers. Older browsers ignore the name and let several stay open.
- The first answer is open on load, so the block never looks empty.
- The plus and minus are aria-hidden; the open state comes from the element itself.
- Focus shows as a 2 pixel outline in the text colour, 4 pixels out from the question.
- Answers ease open only where the browser can animate <details> and motion is allowed; otherwise they simply appear.
Guidelines
Do
- Write questions the way people ask them, and answer in the first sentence.
- Keep it to five or six questions; link to docs for the rest.
Don’t
- Don’t hide information everyone needs inside a closed answer.
- Don’t put links or buttons inside the question itself; the whole row is the control.
In use
Related
- HairlinesA 1px rule that fades out at both ends, and a lit top edge for dark panels.
- Easing & transitionsFour motion curves, a soft cross-fade for view changes, a reduced-motion hook and disclosures that ease open.
- One CommandA developer-first home page: one command to copy, a terminal that types out the scan, a sample report and pricing.
- Command lineA one-line shell command with a $ prompt and a Copy command button.
Source
The component’s own files, exactly as they run on this site.
/** * FAQ as native <details> with a shared `name`, so opening one closes the others * (exclusive accordion, no JS). Keyboard and screen-reader support come for free. * Details: answers ease open (where the browser supports it), the plus turns into a minus, * and rows are separated by hairlines that fade out at the ends. */import { useId } from "react"import { cx } from "../components/cx"import { faqs } from "./content"
export function Faq({ className, items = faqs }: { className?: string; items?: typeof faqs }) { const group = useId() return ( <div className={cx("flex flex-col", className)}> <hr className="lux-rule" /> {items.map((item, i) => ( <div key={item.q}> <details name={group} open={i === 0} className="lux-details group"> <summary className="flex cursor-pointer list-none items-center justify-between gap-6 rounded-md py-6 text-base font-medium tracking-[-0.005em] transition-colors hover:text-neutral-600 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-current dark:hover:text-neutral-300 [&::-webkit-details-marker]:hidden"> {item.q}