Lunia DS

Marketing

Accordion

Questions that open in place: the answer eases open below its question, so the reader keeps their place.

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-accordion

Usage

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.

PropTypeDefaultDescription
items{ question: string; answer: string; id?: string }[]-Questions and answers.
type"single" | "multiple""single"One answer open at a time, or any number.
defaultOpennumber[][]Indexes of items open at first.
EventDescription
openChangeAn item opened or closed. Payload: the list of open indexes.
SlotDescription
answerCustom 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
AC1Open or closeanswer regionheight (grid rows)0 → content heightduration-3xl (460)ease-outResizes at once
AC2Openanswer textopacity, blur0, blur-text → 1, 0duration-2xl (420)ease-outdelay 90msShows at once
AC3Openanswer texttranslate y-distance-nudge → 0duration-4xl (520)ease-outdelay 60msShows at once
AC4Closeanswer textopacity, blur1, 0 → 0, blur-textduration-xs (160)ease-exitHides at once
AC5Closeanswer texttranslate y0 → -distance-nudgeduration-md (260)ease-exitHides at once
AC6Open or closeplus/minus iconrotation0deg → 180degduration-2xl (420)ease-outSwaps at once
AC7Open or closeupright stroke of the plusrotation, scale y0deg, 1 → 90deg, 0.4duration-2xl (420)ease-outSwaps at once
AC10Open or closeupright stroke of the plusopacity1 → 0duration-lg (320)ease-outSwaps at once
AC8Open or closequestion text, iconcolortext-secondary → text-primaryduration-lg (320)ease-outSwaps at once
AC9Another item opens (single)open itemAC1 to AC8 reversedopen → closed, at the same time as the new item opensduration-3xl (460)ease-outSwaps 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.