Les sections FAQ réduisent les tickets de support. Chaque question à laquelle vous répondez sur la page, c'est une question qui n'atterrit pas dans votre boîte mail. Mais un mur de texte ne fonctionne pas — les visiteurs scannent, ils ne lisent pas. Le pattern accordéon résout ce problème en cachant les réponses derrière des questions cliquables, permettant aux utilisateurs de trouver exactement ce qu'ils cherchent.
Ce guide couvre 8 patterns d'accordéon FAQ en React avec Tailwind CSS, d'une implémentation minimale à des layouts animés, catégorisés et multi-colonnes.
1. L'accordéon HTML natif
Avant de sortir du JavaScript, considérez les éléments <details> et <summary> intégrés au navigateur. Ils sont accessibles nativement, ne nécessitent aucune gestion d'état, et fonctionnent même avec JavaScript désactivé :
interface FAQItem {
question: string;
answer: string;
}
function FAQ({ items }: { items: FAQItem[] }) {
return (
<section className="mx-auto max-w-2xl px-6 py-16">
<h2 className="text-3xl font-bold tracking-tight mb-8">
Questions fréquentes
</h2>
<div className="divide-y divide-neutral-200 dark:divide-neutral-800">
{items.map((item) => (
<details key={item.question} className="group py-4">
<summary className="flex cursor-pointer items-center justify-between text-left font-medium">
{item.question}
<span className="ml-4 transition-transform group-open:rotate-45">
+
</span>
</summary>
<p className="mt-3 text-neutral-600 dark:text-neutral-400">
{item.answer}
</p>
</details>
))}
</div>
</section>
);
}
La classe group-open:rotate-45 fait tourner le signe plus quand le détail est ouvert, le transformant en forme de x. Du CSS pur, zéro JavaScript.
2. Accordéon contrôlé avec useState
Quand vous avez besoin qu'un seul élément soit ouvert à la fois (un pattern UX courant), vous avez besoin d'état :
"use client";
import { useState } from "react";
function Accordion({ items }: { items: FAQItem[] }) {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<div className="divide-y divide-neutral-200 dark:divide-neutral-800">
{items.map((item, index) => (
<div key={item.question} className="py-4">
<button
onClick={() => setOpenIndex(openIndex === index ? null : index)}
className="flex w-full items-center justify-between text-left font-medium"
aria-expanded={openIndex === index}
>
{item.question}
<ChevronDown
className={`h-5 w-5 transition-transform ${
openIndex === index ? "rotate-180" : ""
}`}
/>
</button>
{openIndex === index && (
<p className="mt-3 text-neutral-600 dark:text-neutral-400">
{item.answer}
</p>
)}
</div>
))}
</div>
);
}
L'attribut aria-expanded indique aux technologies d'assistance si le contenu est visible. C'est essentiel pour l'accessibilité — ne le sautez pas.
3. Accordéon animé avec Framer Motion
L'accordéon contrôlé ci-dessus a un problème : la réponse apparaît et disparaît instantanément. Ajouter une animation de hauteur avec Framer Motion rend l'interaction plus soignée :
import { AnimatePresence, motion } from "framer-motion";
// Dans le map
{openIndex === index && (
<AnimatePresence>
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.25, ease: [0.16, 1, 0.3, 1] }}
className="overflow-hidden"
>
<p className="pt-3 text-neutral-600 dark:text-neutral-400">
{item.answer}
</p>
</motion.div>
</AnimatePresence>
)}
L'animation height: "auto" est l'une des meilleures fonctionnalités de Framer Motion. Le CSS ne peut pas nativement transitionner vers une hauteur auto — il faudrait du JavaScript pour mesurer l'élément d'abord. Framer Motion gère ça en interne.
4. Accordéon multi-ouverture
Certaines sections FAQ fonctionnent mieux quand plusieurs éléments peuvent être ouverts simultanément. Remplacez le openIndex unique par un Set :
const [openIndexes, setOpenIndexes] = useState<Set<number>>(new Set());
function toggle(index: number) {
setOpenIndexes((prev) => {
const next = new Set(prev);
if (next.has(index)) next.delete(index);
else next.add(index);
return next;
});
}
Utilisez ce pattern quand les éléments FAQ sont courts et que les utilisateurs pourraient vouloir comparer les réponses entre les questions.
5. FAQ deux colonnes
Pour les longues listes FAQ (12+ questions), une seule colonne crée un scroll intimidant. Divisez les éléments en deux colonnes avec CSS grid :
<div className="grid gap-x-12 gap-y-0 md:grid-cols-2">
{items.map((item, index) => (
<AccordionItem key={index} item={item} />
))}
</div>
Les éléments s'écoulent de gauche à droite, de haut en bas — les éléments 1 et 2 partagent la première ligne, 3 et 4 la seconde, et ainsi de suite. Cela divise par deux la longueur perçue de la section.
6. FAQ avec catégories
Les pages FAQ produit couvrent souvent plusieurs sujets : Facturation, Fonctionnalités, Sécurité, Compte. Regroupez les questions sous des en-têtes de catégorie et laissez les utilisateurs filtrer :
const categories = ["Tout", "Facturation", "Fonctionnalités", "Sécurité", "Compte"];
function CategorizedFAQ({ items }: { items: (FAQItem & { category: string })[] }) {
const [activeCategory, setActiveCategory] = useState("Tout");
const filtered = activeCategory === "Tout"
? items
: items.filter((item) => item.category === activeCategory);
return (
<section>
<div className="flex gap-2 mb-8">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setActiveCategory(cat)}
className={`rounded-full px-4 py-1.5 text-sm font-medium transition-colors ${
activeCategory === cat
? "bg-neutral-900 text-white dark:bg-white dark:text-neutral-900"
: "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-400"
}`}
>
{cat}
</button>
))}
</div>
<Accordion items={filtered} />
</section>
);
}
Le sélecteur de catégorie en style pilule est plus invitant qu'un dropdown. Les utilisateurs voient toutes les catégories d'un coup d'oeil et basculent instantanément.
7. FAQ avec recherche
Pour les bases de connaissances avec 50+ questions, ajoutez un champ de recherche au-dessus de l'accordéon. Filtrez les éléments côté client avec un simple check includes sur le texte de la question et de la réponse :
const [query, setQuery] = useState("");
const filtered = items.filter(
(item) =>
item.question.toLowerCase().includes(query.toLowerCase()) ||
item.answer.toLowerCase().includes(query.toLowerCase())
);
Affichez un message "Aucun résultat" quand la liste filtrée est vide. Évitez de cacher le champ de recherche derrière un toggle — si la FAQ est assez longue pour justifier une recherche, le champ doit toujours être visible.
8. FAQ avec balisage structuré
Google peut afficher le contenu FAQ directement dans les résultats de recherche sous forme de rich snippets. Ajoutez des données structurées JSON-LD à votre section FAQ :
function FAQSchema({ items }: { items: FAQItem[] }) {
const schema = {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: items.map((item) => ({
"@type": "Question",
name: item.question,
acceptedAnswer: {
"@type": "Answer",
text: item.answer,
},
})),
};
// Sérialisez le schéma en JSON-LD et injectez-le
// dans une balise <script type="application/ld+json">
return null;
}
Placez ce composant à côté de votre section FAQ. Les données structurées n'affectent pas le rendu visuel mais indiquent aux moteurs de recherche que votre page contient du contenu FAQ éligible aux résultats enrichis.
Checklist accessibilité
- Utilisez des éléments
<button>pour les déclencheurs d'accordéon, pas des<div>aveconClick - Incluez
aria-expandedsur chaque déclencheur - Utilisez
aria-controlsliant le bouton à l'iddu panneau - Assurez-vous que la navigation clavier fonctionne — Entrée et Espace doivent toggler les éléments
- Maintenez des indicateurs de focus visibles sur les déclencheurs
Sections FAQ prêtes à l'emploi
Construire une FAQ accessible et animée from scratch prend du temps que vous pourriez consacrer à votre produit. Le catalogue FAQ d'Incubator propose 10+ sections FAQ prêtes à l'emploi — accordéons, deux colonnes, catégorisées, avec recherche, avec balisage structuré — le tout en React et Tailwind CSS.
Explorez la bibliothèque de composants complète pour chaque section dont votre landing page a besoin, des heroes au pricing.