mdxui

FAQ Section

Frequently asked questions with accordion interface

FAQ Section

The <FAQ> section displays frequently asked questions in an accordion, grid, or list layout. It addresses common concerns and objections before users contact support or abandon the page.

Interface

import type { FAQProps } from 'mdxui'

interface FAQProps {
  /** Section title */
  title?: string
  /** FAQ items */
  items: FAQItem[]
  /** Layout variant */
  variant?: 'accordion' | 'grid' | 'list'
}

interface FAQItem {
  /** Question */
  question: string
  /** Answer */
  answer: string
}

Usage

Basic Example

import { FAQ } from '@mdxui/beacon'

<FAQ
  items={[
    {
      question: "How does the free trial work?",
      answer: "Start with a 14-day free trial. No credit card required. Cancel anytime."
    },
    {
      question: "Can I change plans later?",
      answer: "Yes, you can upgrade or downgrade your plan at any time from your account settings."
    }
  ]}
/>

With Title

<FAQ
  title="Frequently Asked Questions"
  items={[
    {
      question: "What payment methods do you accept?",
      answer: "We accept all major credit cards (Visa, MasterCard, Amex) and PayPal."
    },
    {
      question: "Is there a setup fee?",
      answer: "No setup fees. You only pay for your monthly or annual subscription."
    }
  ]}
/>

Accordion Variant (Default)

<FAQ
  variant="accordion"
  title="Common questions"
  items={[
    {
      question: "How secure is my data?",
      answer: "We use bank-level 256-bit encryption and are SOC 2 Type II certified. Your data is encrypted at rest and in transit."
    },
    {
      question: "Do you offer refunds?",
      answer: "Yes, we offer a 30-day money-back guarantee. If you're not satisfied, contact us for a full refund."
    },
    {
      question: "Can I export my data?",
      answer: "Absolutely. You can export all your data in CSV or JSON format at any time from your dashboard."
    }
  ]}
/>

Grid Variant

<FAQ
  variant="grid"
  title="Got questions? We've got answers"
  items={[
    {
      question: "Is mdxui open source?",
      answer: "Yes, mdxui is MIT licensed and available on GitHub."
    },
    {
      question: "Does it work with Next.js?",
      answer: "Yes, mdxui works with Next.js, Remix, Vite, and any React framework."
    },
    {
      question: "Do I need Payload CMS?",
      answer: "No, the components work standalone. Payload CMS integration is optional."
    },
    {
      question: "What about TypeScript?",
      answer: "Full TypeScript support with Zod schemas for runtime validation."
    }
  ]}
/>

List Variant

<FAQ
  variant="list"
  items={[
    {
      question: "What is your uptime SLA?",
      answer: "We guarantee 99.9% uptime. Our infrastructure is hosted on AWS with multi-region redundancy and automatic failover."
    },
    {
      question: "How does billing work?",
      answer: "You're billed monthly or annually based on your plan. All billing is automatic and you can update your payment method or cancel anytime."
    }
  ]}
/>

Props

title (optional)

Type: string

Section heading displayed above the FAQ items.

<FAQ
  title="Frequently Asked Questions"
  items={[...]}
/>

items (required)

Type: FAQItem[]

Array of question and answer pairs.

interface FAQItem {
  question: string    // The question
  answer: string      // The answer
}

<FAQ
  items={[
    {
      question: "How do I get started?",
      answer: "Sign up for a free account and follow the onboarding wizard."
    }
  ]}
/>

variant (optional)

Type: 'accordion' | 'grid' | 'list' Default: 'accordion'

Controls the layout and interaction style.

// Accordion: Expandable/collapsible (interactive)
<FAQ variant="accordion" />

// Grid: All answers visible in a grid
<FAQ variant="grid" />

// List: All answers visible in a vertical list
<FAQ variant="list" />

FAQ Item Props

question (required)

Type: string

The question being asked. Should be clear and concise.

{
  question: "What happens after my trial ends?"
}

Best practices:

  • Use question format (who, what, when, where, why, how)
  • Keep it under 100 characters
  • Be specific
  • Use customer language

answer (required)

Type: string

The answer to the question. Can be longer and more detailed.

{
  answer: "After your 14-day trial, you'll be prompted to choose a paid plan. You can select from our Starter, Pro, or Enterprise tiers based on your needs."
}

Best practices:

  • Be complete but concise
  • Use 1-3 sentences
  • Include relevant links if needed
  • Be honest and transparent

Variants in Detail

Accordion Variant

Interactive expandable/collapsible sections.

<FAQ
  variant="accordion"
  title="Support"
  items={[
    {
      question: "How do I contact support?",
      answer: "Email us at support@example.com or use the chat widget in the bottom right corner. We typically respond within 2 hours during business hours."
    },
    {
      question: "Do you offer phone support?",
      answer: "Phone support is available for Pro and Enterprise plans. Schedule a call from your dashboard."
    },
    {
      question: "What are your support hours?",
      answer: "Email and chat support: 9am-6pm EST, Monday-Friday. Enterprise customers get 24/7 support."
    }
  ]}
/>

Best for:

  • Many questions (6+)
  • Longer answers
  • Scannable questions

Features:

  • One open at a time (accordion behavior)
  • Smooth expand/collapse animation
  • Keyboard navigation
  • Mobile-friendly

Grid Variant

All questions and answers visible in a card grid.

<FAQ
  variant="grid"
  title="Quick answers"
  items={[
    {
      question: "Is it free?",
      answer: "Free 14-day trial, then starting at $29/mo."
    },
    {
      question: "Mobile app?",
      answer: "iOS and Android apps available."
    },
    {
      question: "Integrations?",
      answer: "Integrates with Slack, Zapier, and 50+ tools."
    },
    {
      question: "Team features?",
      answer: "Real-time collaboration, comments, and permissions."
    }
  ]}
/>

Best for:

  • Short answers (1 sentence)
  • Quick scanning
  • 4-8 questions

List Variant

All questions and answers visible in a vertical list.

<FAQ
  variant="list"
  title="Billing questions"
  items={[
    {
      question: "When will I be charged?",
      answer: "You'll be charged at the end of your trial period, then monthly or annually depending on your plan selection."
    },
    {
      question: "Can I get an invoice?",
      answer: "Yes, invoices are automatically sent to your email after each payment. You can also download them from your billing page."
    },
    {
      question: "What currency do you charge in?",
      answer: "All prices are in USD. International cards are accepted and your bank will handle the currency conversion."
    }
  ]}
/>

Best for:

  • Detailed answers
  • Few questions (3-5)
  • Print-friendly

Implementation Details

The reference implementation from @mdxui/beacon includes:

Features

  • Accordion Animation - Smooth expand/collapse
  • Keyboard Navigation - Arrow keys to navigate
  • Accessible - ARIA labels and semantic HTML
  • Responsive - Mobile-optimized
  • Single Open - Only one accordion item open at a time
  • Deep Linking - Link to specific questions (optional)

Structure

<section className="faq">
  {title && (
    <div className="faq-header">
      <h2>{title}</h2>
    </div>
  )}

  {variant === 'accordion' && (
    <div className="faq-accordion">
      {items.map((item, index) => (
        <details key={index} className="faq-item">
          <summary className="question">
            {item.question}
            <ChevronDown className="icon" />
          </summary>
          <div className="answer">
            <p>{item.answer}</p>
          </div>
        </details>
      ))}
    </div>
  )}

  {variant === 'grid' && (
    <div className="faq-grid">
      {items.map((item, index) => (
        <div key={index} className="faq-item">
          <h3 className="question">{item.question}</h3>
          <p className="answer">{item.answer}</p>
        </div>
      ))}
    </div>
  )}

  {variant === 'list' && (
    <dl className="faq-list">
      {items.map((item, index) => (
        <div key={index} className="faq-item">
          <dt className="question">{item.question}</dt>
          <dd className="answer">{item.answer}</dd>
        </div>
      ))}
    </dl>
  )}
</section>

Real-World Examples

SaaS Product FAQ

<FAQ
  title="Frequently Asked Questions"
  variant="accordion"
  items={[
    {
      question: "How does the free trial work?",
      answer: "Sign up for a 14-day free trial with full access to all features. No credit card required. At the end of the trial, choose a plan or your account will be downgraded to the free tier."
    },
    {
      question: "Can I cancel anytime?",
      answer: "Yes, you can cancel your subscription at any time. Your account will remain active until the end of your billing period."
    },
    {
      question: "What happens to my data if I cancel?",
      answer: "Your data remains accessible for 30 days after cancellation. You can export everything during this time. After 30 days, data is permanently deleted."
    },
    {
      question: "Do you offer discounts for nonprofits or education?",
      answer: "Yes! We offer 50% off for verified nonprofits and educational institutions. Contact sales@example.com with your documentation."
    },
    {
      question: "Is my data secure?",
      answer: "Absolutely. We use bank-level encryption, are SOC 2 Type II certified, and GDPR compliant. Your data is encrypted at rest and in transit."
    },
    {
      question: "Can I upgrade or downgrade my plan?",
      answer: "Yes, change plans anytime from your account settings. Upgrades take effect immediately. Downgrades take effect at the end of your current billing period."
    }
  ]}
/>

Developer Platform FAQ

<FAQ
  title="Developer FAQ"
  variant="grid"
  items={[
    {
      question: "What languages do you support?",
      answer: "JavaScript, TypeScript, Python, Ruby, Go, and PHP."
    },
    {
      question: "Is there a CLI?",
      answer: "Yes, install via npm: npm install -g @example/cli"
    },
    {
      question: "Do you have webhooks?",
      answer: "Yes, configure webhooks in your dashboard to receive real-time events."
    },
    {
      question: "What's the API rate limit?",
      answer: "1000 requests/hour on Free, 10k/hour on Pro, unlimited on Enterprise."
    },
    {
      question: "Do you support GraphQL?",
      answer: "Yes, we offer both REST and GraphQL APIs."
    },
    {
      question: "Where's the documentation?",
      answer: "Full docs at docs.example.com with examples in all supported languages."
    }
  ]}
/>

E-commerce FAQ

<FAQ
  title="Shopping FAQ"
  variant="list"
  items={[
    {
      question: "What is your return policy?",
      answer: "We accept returns within 30 days of purchase for a full refund. Items must be unused and in original packaging. Free return shipping on all orders."
    },
    {
      question: "How long does shipping take?",
      answer: "Standard shipping: 5-7 business days. Express shipping: 2-3 business days. Free shipping on orders over $50."
    },
    {
      question: "Do you ship internationally?",
      answer: "Yes, we ship to over 100 countries. International orders typically arrive in 10-14 business days. Customs fees may apply."
    },
    {
      question: "How can I track my order?",
      answer: "You'll receive a tracking number via email once your order ships. Track your package at any time from your account dashboard."
    }
  ]}
/>

Accordion Behavior

Single vs Multiple Open

Most implementations allow only one item open at a time:

const [openIndex, setOpenIndex] = useState<number | null>(null)

const toggle = (index: number) => {
  setOpenIndex(openIndex === index ? null : index)
}

For multiple open items:

const [openIndexes, setOpenIndexes] = useState<Set<number>>(new Set())

const toggle = (index: number) => {
  const newSet = new Set(openIndexes)
  if (newSet.has(index)) {
    newSet.delete(index)
  } else {
    newSet.add(index)
  }
  setOpenIndexes(newSet)
}

Using HTML Details

The simplest implementation uses native <details>:

<details>
  <summary>{question}</summary>
  <div>{answer}</div>
</details>

Pros:

  • Native HTML
  • No JavaScript required
  • Accessible by default

Cons:

  • Limited styling control
  • Can't enforce single-open
  • Animation support varies

Accessibility

The reference implementation includes:

  • Semantic HTML - Uses <section>, <details>, <dl>, <dt>, <dd>
  • ARIA Labels - Proper labels for interactive elements
  • Keyboard Navigation - Tab, Enter, Space keys work
  • Focus Management - Visible focus indicators
  • Screen Readers - Announces expanded/collapsed state
<section className="faq" aria-labelledby="faq-title">
  <h2 id="faq-title">{title}</h2>

  {/* Accordion variant */}
  <div className="accordion" role="region">
    {items.map((item, index) => (
      <div className="accordion-item" key={index}>
        <h3>
          <button
            aria-expanded={openIndex === index}
            aria-controls={`faq-answer-${index}`}
            id={`faq-question-${index}`}
            onClick={() => toggle(index)}
          >
            {item.question}
          </button>
        </h3>
        <div
          id={`faq-answer-${index}`}
          aria-labelledby={`faq-question-${index}`}
          hidden={openIndex !== index}
        >
          <p>{item.answer}</p>
        </div>
      </div>
    ))}
  </div>

  {/* List variant using definition list */}
  <dl className="faq-list">
    {items.map((item, index) => (
      <div key={index}>
        <dt>{item.question}</dt>
        <dd>{item.answer}</dd>
      </div>
    ))}
  </dl>
</section>

Best Practices

Choosing Questions

  • Use real questions: From customers, support tickets
  • Address objections: Price, features, security
  • Be comprehensive: Cover full customer journey
  • Update regularly: Add new questions as they arise
  • Prioritize: Most important/common first

Writing Questions

  • Use customer language: How they ask, not how you'd say it
  • Be specific: "How does billing work?" > "Billing?"
  • Keep it short: Under 100 characters
  • Use keywords: For SEO and search

Writing Answers

  • Be complete: Answer fully, don't make them ask follow-ups
  • Be concise: 1-3 sentences ideal
  • Be honest: Don't oversell or hide limitations
  • Add links: Link to detailed docs if needed
  • Use formatting: Bold key points if answer is long

Organization

  • Group by topic: Billing, Features, Support, etc.
  • Order by importance: Most common questions first
  • Limit total: 6-12 questions ideal, more needs search
  • Consider multiple sections: Break up long FAQs

Rich Content in Answers

For answers with formatting, links, or lists:

interface FAQItem {
  question: string
  answer: string | ReactNode  // Allow React elements
}

<FAQ
  items={[
    {
      question: "What payment methods do you accept?",
      answer: (
        <>
          <p>We accept:</p>
          <ul>
            <li>Credit cards (Visa, MasterCard, Amex)</li>
            <li>PayPal</li>
            <li>ACH bank transfer (Enterprise only)</li>
          </ul>
          <p>Learn more in our <a href="/billing">billing docs</a>.</p>
        </>
      )
    }
  ]}
/>

Search Integration

For large FAQs, add search functionality:

const [searchTerm, setSearchTerm] = useState('')

const filteredItems = items.filter(item =>
  item.question.toLowerCase().includes(searchTerm.toLowerCase()) ||
  item.answer.toLowerCase().includes(searchTerm.toLowerCase())
)

<div className="faq">
  <input
    type="search"
    placeholder="Search FAQs..."
    value={searchTerm}
    onChange={(e) => setSearchTerm(e.target.value)}
  />

  <FAQ items={filteredItems} />
</div>

Composition

In LandingPage

FAQ typically appears near the end, before final CTA:

<LandingPage>
  <Hero {...heroProps} />
  <Features {...featuresProps} />
  <Pricing {...pricingProps} />
  <Testimonials {...testimonialsProps} />
  <FAQ {...faqProps} />
  <CTA {...ctaProps} />
</LandingPage>

With CTA

End with CTA for users with remaining questions:

<FAQ {...faqProps} />
<CTA
  title="Still have questions?"
  description="Our team is here to help"
  cta={{ text: "Contact Support", href: "/contact" }}
/>

Multiple FAQ Sections

Break up by category:

<FAQ
  title="Product Questions"
  items={productQuestions}
/>

<FAQ
  title="Billing Questions"
  items={billingQuestions}
/>

<FAQ
  title="Technical Questions"
  items={technicalQuestions}
/>

TypeScript

Full type definitions:

import { z } from 'zod'
import type { Component } from 'mdxui'

export const FAQItemSchema = z.object({
  question: z.string(),
  answer: z.string(),
})

export const FAQPropsSchema = z.object({
  title: z.string().optional(),
  items: z.array(FAQItemSchema),
  variant: z.enum(['accordion', 'grid', 'list']).default('accordion'),
})

export type FAQItem = z.infer<typeof FAQItemSchema>
export type FAQProps = z.infer<typeof FAQPropsSchema>
export type FAQ = Component<FAQProps>

On this page