Skip to main content

Overview

The FAQ component displays frequently asked questions in an accordion format, allowing users to expand and collapse individual questions. It supports custom styling and controlled/uncontrolled modes.

Location

~/workspace/source/components/Faq.tsx

Features

  • Accordion interaction - One question open at a time
  • Smooth animations - Content slides in/out gracefully
  • Data-driven - Content from faq.data.ts
  • Accessible - ARIA attributes for screen readers
  • Blueprint design - Dashed dividers and subtle hover states

Component Structure

import FAQSection from '@/components/Faq';
import { FAQS } from '@/components/faq/faq.data';

<FAQSection 
  items={FAQS}
  defaultOpenId="first-question"
/>

Props

items
FAQItem[]
required
Array of FAQ items to display. Each item must have an id, q (question), and a (answer).
defaultOpenId
string
The ID of the FAQ to open by default. If not provided, the first item opens automatically.

FAQItem Type

type FAQItem = {
  id: string;          // Unique identifier
  q: string;           // Question text
  a: string | string[]; // Answer (single paragraph or array)
};

Data Structure

FAQ content is defined in ~/components/faq/faq.data.ts:
import type { FAQItem } from './faq.data';

export const FAQS: FAQItem[] = [
  {
    id: 'what-services',
    q: '¿Qué servicios ofreces?',
    a: 'Diseño UX/UI completo: desde investigación y arquitectura de información hasta prototipos interactivos y diseño visual.'
  },
  {
    id: 'how-long',
    q: '¿Cuánto tiempo toma un proyecto?',
    a: [
      'Depende del alcance, pero en promedio:',
      '• Landing page: 1-2 semanas',
      '• MVP o app pequeña: 3-6 semanas',
      '• Plataforma completa: 8-12 semanas'
    ]
  },
  // ... more questions
];
Answers can be a single string for simple responses, or an array of strings for multi-paragraph or bulleted answers.

Interaction Logic

From Faq.tsx:15-19:
const [openId, setOpenId] = useState<string | null>(initial);

const onToggle = (id: string) => {
  setOpenId((curr) => (curr === id ? null : id));
};
  • Clicking an open question closes it
  • Clicking a closed question opens it and closes others
  • Only one question can be open at a time

Styling

Accordion Structure

<ul className="divide-y divide-dashed divide-neutral-white/15">
  {items.map((it) => (
    <li key={it.id} className="py-8">
      <button
        type="button"
        onClick={() => onToggle(it.id)}
        aria-expanded={isOpen}
        aria-controls={`faq-panel-${it.id}`}
      >
        {/* Question */}
      </button>
      
      {isOpen && (
        <div id={`faq-panel-${it.id}`}>
          {/* Answer */}
        </div>
      )}
    </li>
  ))}
</ul>

Typography

  • Question: text-[clamp(1rem,1.25vw,1.25rem)] with semibold weight
  • Answer: text-[14px] md:text-[15px] with relaxed line-height
  • Dividers: Dashed borders at 15% opacity

Accessibility

ARIA Attributes

<button
  aria-expanded={isOpen}
  aria-controls={`faq-panel-${it.id}`}
>
  {question}
</button>

<div 
  id={`faq-panel-${it.id}`}
  role="region"
  aria-labelledby={`faq-button-${it.id}`}
>
  {answer}
</div>
  • aria-expanded indicates open/closed state
  • aria-controls links button to panel
  • Semantic <button> element for keyboard access

Usage Example

From app/page.tsx:47:
import FAQSection from '@/components/Faq';
import { FAQS } from '@/components/faq/faq.data';

export default function Home() {
  return (
    <main>
      {/* Other sections */}
      <FAQSection items={FAQS} />
      {/* More sections */}
    </main>
  );
}

Customization

Adding New Questions

Edit ~/components/faq/faq.data.ts:
export const FAQS: FAQItem[] = [
  // ... existing questions
  {
    id: 'new-question',
    q: '¿Nueva pregunta aquí?',
    a: 'Respuesta detallada aquí...'
  }
];

Multi-Paragraph Answers

{
  id: 'detailed-answer',
  q: '¿Pregunta compleja?',
  a: [
    'Primer párrafo de la respuesta.',
    'Segundo párrafo con más detalles.',
    '• Punto 1',
    '• Punto 2'
  ]
}

Custom Default Open

<FAQSection 
  items={FAQS}
  defaultOpenId="most-important-question"
/>

Responsive Behavior

BreakpointPaddingQuestion Font
Mobilepx-6clamp(1rem,1.25vw,1.25rem)
Tabletpx-8Same
Desktoppx-12Same
Largexl:px-32Same

Animation Details

The answer panel uses CSS transitions:
<div 
  className="mt-4 overflow-hidden transition-all duration-300"
  style={{
    maxHeight: isOpen ? '1000px' : '0',
    opacity: isOpen ? 1 : 0
  }}
>
  {/* Answer content */}
</div>

Best Practices

Keep questions concise (1-2 lines) and answers focused. If an answer is getting long, consider splitting into multiple questions.
Avoid adding too many FAQs (>10) as users may not scroll through them all. Prioritize the most common questions.
The component automatically opens the first question if no defaultOpenId is provided, giving users an immediate example of the interaction pattern.

SEO Considerations

The FAQ section uses semantic HTML and can be enhanced with FAQ schema markup:
// In SeoJsonLd.tsx or similar
{
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "¿Qué servicios ofreces?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Diseño UX/UI completo..."
      }
    }
  ]
}

Build docs developers (and LLMs) love