Marketing
Accordion
Questions that open in place: the answer eases open below its question, so the reader keeps their place.
Live preview
Loading preview
Four questions about plans and billing, in single mode with the first one open. Replay closes all and opens the second question.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add faq-accordionUsage
The demo above, as code.
demo.tsx
'use client';
import * as React from 'react';
import { FaqAccordion } from './faq-accordion';
const ITEMS = [
{
question: 'Can I change my plan later?',
answer:
'Yes. You can move to a larger or smaller plan at any time, and the new price starts on your next bill.',
},
{
question: 'Is there a free trial?',
answer: 'Every plan starts with 14 free days. We ask for a card only when the trial ends.',
},
{
question: 'How does yearly billing work?',
answer: 'You pay for twelve months at once and save 20% compared with paying each month.',
},
{
question: 'Can I cancel at any time?',
answer:
'Yes. Your plan stays active until the end of the period you paid for, and we do not charge you again.',
},
];
export default function FaqAccordionDemo({ replay = 0 }: { replay?: number }) {
const ref = React.useRef<HTMLDivElement>(null);
// Replay closes the open answer, then opens the second question, the same way a click does.
React.useEffect(() => {
if (replay === 0) return;
const triggers = () => [
...(ref.current?.querySelectorAll<HTMLButtonElement>('[data-slot="faq-accordion"] h3 button') ?? []),
];
for (const trigger of triggers()) if (trigger.hasAttribute('data-open')) trigger.click();
const timer = window.setTimeout(() => triggers()[1]?.click(), 500);
return () => window.clearTimeout(timer);
}, [replay]);
return (
<div ref={ref} className="w-full max-w-md">
<FaqAccordion items={ITEMS} defaultOpen={[0]} />
</div>
);
}
API
Parts: root (list), item, question (button), plus/minus icon, answer region, answer text, divider.
| Prop | Type | Default | Description |
|---|---|---|---|
| items | { question: string; answer: string; id?: string }[] | - | Questions and answers. |
| type | "single" | "multiple" | "single" | One answer open at a time, or any number. |
| defaultOpen | number[] | [] | Indexes of items open at first. |
| Event | Description |
|---|---|
| openChange | An item opened or closed. Payload: the list of open indexes. |
| Slot | Description |
|---|---|
| answer | Custom content for an answer instead of plain text. |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| AC1 | Open or close | answer region | height (grid rows) | 0 → content height | duration-3xl (460)ease-out | Resizes at once |
| AC2 | Open | answer text | opacity, blur | 0, blur-text → 1, 0 | duration-2xl (420)ease-outdelay 90ms | Shows at once |
| AC3 | Open | answer text | translate y | -distance-nudge → 0 | duration-4xl (520)ease-outdelay 60ms | Shows at once |
| AC4 | Close | answer text | opacity, blur | 1, 0 → 0, blur-text | duration-xs (160)ease-exit | Hides at once |
| AC5 | Close | answer text | translate y | 0 → -distance-nudge | duration-md (260)ease-exit | Hides at once |
| AC6 | Open or close | plus/minus icon | rotation | 0deg → 180deg | duration-2xl (420)ease-out | Swaps at once |
| AC7 | Open or close | upright stroke of the plus | rotation, scale y | 0deg, 1 → 90deg, 0.4 | duration-2xl (420)ease-out | Swaps at once |
| AC10 | Open or close | upright stroke of the plus | opacity | 1 → 0 | duration-lg (320)ease-out | Swaps at once |
| AC8 | Open or close | question text, icon | color | text-secondary → text-primary | duration-lg (320)ease-out | Swaps at once |
| AC9 | Another item opens (single) | open item | AC1 to AC8 reversed | open → closed, at the same time as the new item opens | duration-3xl (460)ease-out | Swaps at once |
Accessibility
- Each question is a button with aria-expanded and aria-controls pointing to its answer.
- Each answer is role="region" with aria-labelledby pointing to its question.
- Up and Down arrow keys move between questions; Home and End jump to the first and last.
- The plus/minus icon is aria-hidden.