FAQ accordion
A frequently asked questions section where each answer expands from an accessible disclosure button.
cmp_faq_accordion_001 Preview
Fit to the available width. Previews taller than the maximum auto-height scroll inside the frame.
Use this component with your coding agent
Using the PageSugar MCP server, fetch component cmp_faq_accordion_001 version 1.0.0 with variant "neutral", then add its files to this project and follow its usage notes.
Code
Neutral palette, version 1.0.0. Artifact sha256-23239c116daa334fa144c0d3418b21b6a5df93bc18f332c156ce8b0e0dddcfe0
FaqAccordion.svelte · Svelte component · 2.7 KB
<script module lang="ts">
export interface FaqItem {
question: string;
/** Plain text answer. Line breaks are preserved. */
answer: string;
}
</script>
<script lang="ts">
interface Props {
items: FaqItem[];
title: string;
description?: string;
headingLevel?: 2 | 3 | 4 | 5;
multiple?: boolean;
defaultOpen?: number[];
}
let {
items,
title,
description,
headingLevel = 2,
multiple = false,
defaultOpen = []
}: Props = $props();
const uid = $props.id();
const sectionHeading = $derived(`h${headingLevel}`);
const itemHeading = $derived(`h${headingLevel + 1}`);
function initialOpen(): number[] {
return multiple ? [...defaultOpen] : defaultOpen.slice(0, 1);
}
let open = $state<number[]>(initialOpen());
function toggle(index: number) {
if (open.includes(index)) {
open = open.filter((i) => i !== index);
} else {
open = multiple ? [...open, index] : [index];
}
}
</script>
<section class="px-4 py-16 sm:px-6 lg:px-8" aria-labelledby="{uid}-title">
<div class="mx-auto max-w-3xl">
<svelte:element
this={sectionHeading}
id="{uid}-title"
class="text-3xl font-semibold tracking-tight text-balance text-zinc-900 sm:text-4xl"
>
{title}
</svelte:element>
{#if description}
<p class="mt-4 text-base text-pretty text-zinc-600 sm:text-lg">{description}</p>
{/if}
<div class="mt-10 divide-y divide-zinc-200 border-y border-zinc-200">
{#each items as item, index (index)}
{@const expanded = open.includes(index)}
<div>
<svelte:element this={itemHeading} class="m-0 text-base font-medium">
<button
type="button"
id="{uid}-trigger-{index}"
aria-expanded={expanded}
aria-controls="{uid}-panel-{index}"
onclick={() => toggle(index)}
class="flex w-full items-start justify-between gap-6 py-5 text-left text-zinc-900 hover:text-zinc-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-900"
>
<span class="min-w-0 break-words">{item.question}</span>
<svg
class={[
'mt-0.5 size-5 shrink-0 text-zinc-500 transition-transform motion-reduce:transition-none',
expanded && 'rotate-180'
]}
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
>
<path
d="M5 7.5l5 5 5-5"
stroke="currentColor"
stroke-width="1.75"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</svelte:element>
<div id="{uid}-panel-{index}" hidden={!expanded} class="pr-11 pb-5">
<p class="text-base break-words whitespace-pre-line text-zinc-600">{item.answer}</p>
</div>
</div>
{/each}
</div>
</div>
</section>
Usage
Supply questions and plain-text answers. Open/closed state is local to each instance and is not persisted, synced to the URL or reported through callbacks. No runtime dependencies and no backend.
Suggested location: src/lib/components/faq-accordion-01
Required props: items, title
Example
<script lang="ts">
import FaqAccordion, { type FaqItem } from '$lib/components/faq-accordion-01/FaqAccordion.svelte';
const items: FaqItem[] = [
{ question: 'Can I change plans later?', answer: 'Yes. Plan changes apply from your next billing date.' },
{ question: 'Is there a free trial?', answer: 'Every plan includes a 14-day trial.' }
];
</script>
<FaqAccordion title="Frequently asked questions" {items} defaultOpen={[0]} />Limitations
- Answers are plain text; edit the source to render rich content or snippets.
- Collapsed answers are hidden until JavaScript runs; content in closed panels is not reachable without client-side hydration.
- defaultOpen is read once at creation; changing multiple later does not collapse answers already open, and open state is tracked by item position. Remount (e.g. with {#key}) after changing multiple or reordering items.
FAQ accordion #
A questions-and-answers section. Each question is a disclosure button that shows or hides its answer. Open/closed state lives inside each instance only; nothing is persisted or sent anywhere. No runtime dependencies.
Install #
Copy FaqAccordion.svelte into src/lib/components/faq-accordion-01/. No packages are required.
Example #
<script lang="ts">
import FaqAccordion, { type FaqItem } from '$lib/components/faq-accordion-01/FaqAccordion.svelte';
const items: FaqItem[] = [
{
question: 'Can I change plans later?',
answer: 'Yes. Plan changes apply from your next billing date.'
},
{ question: 'Is there a free trial?', answer: 'Every plan includes a 14-day trial.' }
];
</script>
<FaqAccordion title="Frequently asked questions" {items} defaultOpen={[0]} />Props #
| Prop | Type | Default | Notes |
|---|---|---|---|
items |
FaqItem[] |
required | { question: string; answer: string }. |
title |
string |
required | Section heading. |
description |
string |
— | Intro paragraph. |
headingLevel |
2 | 3 | 4 | 5 |
2 |
Questions are wrapped in headingLevel + 1. |
multiple |
boolean |
false |
Allow several answers open at once. |
defaultOpen |
number[] |
[] |
Indexes open on first render (first only if single). |
Behaviour #
- Clicking a question (or pressing Enter/Space on it) toggles its answer.
- With
multipleoff, opening one answer closes the previous one. defaultOpenis read once at creation, and changingmultiplelater does not collapse answers that are already open. State is tracked by index, so remount the component (for example with{#key}) after changingmultipleor reorderingitems.
Customization #
- Colours: edit the
zinctext, divider and outline utilities in the source. - Width: change
max-w-3xlon the inner wrapper. - Rich answers: change
answerto a snippet or markup and replace the panel paragraph.
Limitations #
- Answers are plain text.
- Closed answers stay hidden until JavaScript hydrates.
- No arrow-key navigation between questions (optional in the APG pattern).
- Light appearance only.
Customization
Change content through props and edit Tailwind classes in the source for colours, spacing and dividers. No colour tokens are declared.
- Content: pass items as { question, answer }; line breaks in answers are preserved.
- Headings: set headingLevel so the section fits the page outline; questions use the next level down.
- Behaviour: set multiple to allow several open answers; use defaultOpen to expand answers on load.
- Colours: text and divider colours are zinc utilities on the section, buttons and panels; replace them together and keep body text at 4.5:1 contrast.
- Width: the content column uses max-w-3xl; change it on the inner wrapper.
- Rich answers: change the answer type and replace the paragraph in the panel with your own markup or a snippet prop.
Props and content inputs
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
items | FaqItem[] | Yes | None | Questions and plain-text answers ({ question: string; answer: string }) in display order. |
title | string | Yes | None | Section heading text. |
description | string | No | None | Optional introductory paragraph under the heading. |
headingLevel | 2 | 3 | 4 | 5 | No | 2 | Level of the section heading; each question is wrapped in a heading one level lower. |
multiple | boolean | No | false | When false, opening one answer closes the others. When true, answers open independently. |
defaultOpen | number[] | No | [] | Zero-based indexes of answers expanded on first render. Only the first index is used when multiple is false. |
Dependencies and services
No additional runtime packages beyond Svelte and Tailwind CSS.
Integration boundaries
- Integration level: local-interaction.
- Requires client-side JavaScript to be interactive.
- Server-side rendering: supported.
Accessibility
- Follows the WAI-ARIA disclosure/accordion pattern: each question is a native button inside a heading, with aria-expanded and aria-controls pointing at its answer panel.
- Buttons are operable with Enter and Space and appear in normal Tab order; collapsed panels use the hidden attribute so their content is removed from the accessibility tree.
- IDs are derived from $props.id(), so several accordions on one page never collide and server and client markup match.
- The chevron is decorative and hidden from assistive technology; state is conveyed by aria-expanded.
- Focus is shown with a visible outline; the chevron animation is disabled when reduced motion is requested.
Known limitations
- Arrow-key navigation between questions is not implemented (it is optional in the APG accordion pattern).
License
Source license: MIT.
Version history
Only one release has been published so far; earlier versions will be listed here.
- 1.0.0 Published current · selected · 16 September 2026