Une FAQ en accordéon (la question s'affiche, la réponse se déplie au clic) est l'un des meilleurs premiers projets pour apprendre à faire travailler ensemble HTML, CSS et JavaScript. Tu vas construire une FAQ propre, qui fonctionne à la souris et au clavier, et qui reste lisible par les lecteurs d'écran. Durée : une trentaine de minutes.
Ce que tu vas construire
- une liste de questions ; un clic sur une question affiche sa réponse ;
- une seule réponse ouverte à la fois : ouvrir une question ferme les autres ;
- un « + » qui pivote pour indiquer l'état ouvert ou fermé ;
- une FAQ accessible : utilisable au clavier (Tab, Entrée, Espace) et annoncée correctement par les lecteurs d'écran.
Crée un dossier avec trois fichiers : index.html, style.css et script.js.
1. La structure HTML
Chaque question est un bouton, placé dans un titre. C'est le point le plus important du projet : un <button> est cliquable, atteignable avec Tab et activable avec Entrée ou Espace, sans une ligne de code en plus. Avec un simple <h2> cliquable, les personnes qui naviguent au clavier ne pourraient pas ouvrir les réponses.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FAQ</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="faq">
<h1>Questions fréquentes</h1>
<div class="faq-item">
<h2>
<button class="faq-question" aria-expanded="false" aria-controls="reponse-1" id="question-1">
Quels sont les délais de livraison ?
</button>
</h2>
<div class="faq-answer" id="reponse-1" role="region" aria-labelledby="question-1" hidden>
<p>Les commandes partent sous 48 heures et arrivent en 3 à 5 jours ouvrés.</p>
</div>
</div>
<div class="faq-item">
<h2>
<button class="faq-question" aria-expanded="false" aria-controls="reponse-2" id="question-2">
Comment contacter le support ?
</button>
</h2>
<div class="faq-answer" id="reponse-2" role="region" aria-labelledby="question-2" hidden>
<p>Par e-mail ou via le chat, du lundi au vendredi de 9 h à 18 h.</p>
</div>
</div>
</main>
<script src="script.js"></script>
</body>
</html>
Trois attributs font le travail d'accessibilité :
aria-expandedindique si la réponse est ouverte (true) ou fermée (false) ;aria-controlsrelie le bouton à sa réponse, grâce à l'idde celle-ci ;hiddencache la réponse, pour tout le monde, lecteurs d'écran compris.
2. Le style en CSS
On retire l'apparence par défaut des boutons pour qu'ils ressemblent à des questions, et on ajoute un « + » qui pivote en « × » quand la réponse est ouverte. Le style s'appuie directement sur aria-expanded : pas besoin de classe en plus, l'état est déjà écrit dans le HTML.
.faq {
max-width: 40rem;
margin: 2rem auto;
padding: 0 1rem;
font-family: system-ui, sans-serif;
}
.faq-item {
border-bottom: 1px solid #ddd;
}
.faq-item h2 {
margin: 0;
font-size: 1.1rem;
}
.faq-question {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1rem 0;
border: 0;
background: none;
font: inherit;
text-align: left;
cursor: pointer;
}
.faq-question::after {
content: "+";
font-size: 1.5rem;
transition: transform 0.2s;
}
.faq-question[aria-expanded="true"]::after {
transform: rotate(45deg);
}
.faq-question:focus-visible {
outline: 2px solid #25737d;
outline-offset: 2px;
}
.faq-answer {
padding-bottom: 1rem;
color: #444;
}
Ne supprime jamais le contour de focus sans le remplacer : :focus-visible l'affiche seulement pour la navigation au clavier, là où il est utile.
3. L'interaction en JavaScript
Au clic sur une question, on ferme toutes les réponses, puis on ouvre celle de la question cliquée, sauf si elle était déjà ouverte : dans ce cas, le clic la referme.
const questions = document.querySelectorAll('.faq-question');
questions.forEach((question) => {
question.addEventListener('click', () => {
const isOpen = question.getAttribute('aria-expanded') === 'true';
// Une seule réponse ouverte à la fois : on ferme les autres.
questions.forEach((other) => {
other.setAttribute('aria-expanded', 'false');
document.getElementById(other.getAttribute('aria-controls')).hidden = true;
});
question.setAttribute('aria-expanded', String(!isOpen));
document.getElementById(question.getAttribute('aria-controls')).hidden = isOpen;
});
});
Pourquoi hidden plutôt que style.display ? C'est un attribut HTML standard, compris par les navigateurs et les technologies d'assistance, et il garde le JavaScript simple : true pour cacher, false pour afficher.
4. Tester ta FAQ
- À la souris : ouvre une question, puis une autre : la première doit se fermer.
- Au clavier : recharge la page, appuie sur Tab pour atteindre la première question, puis sur Entrée ou Espace.
- Dans la console (F12) : aucune erreur ne doit s'afficher. Si une réponse ne s'ouvre pas, vérifie que l'
idde la réponse correspond bien à l'aria-controlsdu bouton.
L'alternative sans JavaScript : details et summary
HTML propose un accordéon natif, sans une ligne de JavaScript :
<details class="faq-item">
<summary>Quels sont les délais de livraison ?</summary>
<p>Les commandes partent sous 48 heures et arrivent en 3 à 5 jours ouvrés.</p>
</details>
C'est la bonne solution pour une FAQ simple. La version en JavaScript reste un excellent exercice, et elle permet ce que <details> ne fait pas seul : n'ouvrir qu'une réponse à la fois.
Pour aller plus loin
- Animer l'ouverture : remplace
hiddenpar une transition surgrid-template-rows(de0frà1fr) pour déplier la réponse en douceur. - Générer la FAQ depuis des données : écris les questions dans un tableau JavaScript, et crée les éléments avec une boucle. C'est la première marche vers les frameworks comme React.
- Ajouter une recherche : un champ qui masque les questions qui ne contiennent pas le texte tapé.
Tu as terminé ta FAQ ? Passe au projet suivant du parcours, juste en dessous.