Quelque part en production, une page dont l’« accordéon » de FAQ supposément simple charge 180 KB de JavaScript, bloque le rendu et rate la navigation clavier. Vous le sentez : la patience de l’utilisateur qui s’en va, votre score Lighthouse qui gémit, et un futur ticket en on-call intitulé « L’accordéon ne s’ouvre pas sur iPad. »
Voici la meilleure approche : livrer en HTML sémantique d’abord, puis améliorer. Utilisez <details>/<summary> quand c’est pertinent, et n’écrivez du JavaScript que lorsqu’il a vraiment une valeur ajoutée.
L’approche : amélioration progressive, pas regret progressif
Quand vous construisez des composants UI pour le web, vous n’expédiez pas seulement des pixels. Vous expédiez des modes de défaillance : comment la page se comporte quand le JavaScript charge en retard, quand le CSS ne charge pas, quand l’utilisateur navigue au clavier, quand le navigateur est ancien, quand un proxy d’entreprise injecte des absurdités, ou quand votre propre bundler double silencieusement la charge parce que quelqu’un a importé une utilité du « package UI partagé ».
L’amélioration progressive est la discipline ennuyeuse qui consiste à faire fonctionner l’expérience de base avec le moins d’hypothèses possible. Ensuite vous l’améliorez pour la capacité, pas pour la mode. La base doit être lisible, navigable et utilisable avec la pile la plus primitive que vous tolérez : HTML rendu côté serveur, CSS minimal, et zéro dépendance JavaScript pour l’interaction de base quand c’est possible.
Opinion : Si votre accordéon nécessite un framework JavaScript pour s’ouvrir, ce n’est pas un accordéon. C’est une enseigne lumineuse qui dit « je suis une machine à états sans plan B. »
Le HTML natif <details>/<summary> est l’histoire d’amélioration progressive la plus propre pour les divulgations et les accordéons. Les onglets sont plus délicats : il n’existe pas d’élément natif « onglet », et essayer de forcer <details> dans une sémantique d’onglet vous donne une UI qui ressemble à des onglets mais se lit comme un tas de bascules.
Deux objectifs guident tout ici :
- Résilience : si les scripts échouent, le contenu reste accessible et l’interaction garde du sens.
- Observabilité : si ça casse, vous pouvez le diagnostiquer rapidement avec des outils normaux, pas un prêtre et un bundle minifié.
Et oui, vous pouvez toujours avoir de belles animations, le deep-linking par URL, et le comportement « une seule ouverture ». Il suffit de le gagner progressivement.
Une citation pour la route, parce que c’est toujours vrai en UI : « L’espoir n’est pas une stratégie. » — Vince Lombardi
Blague #1 : Une bibliothèque UI, c’est comme une plante d’intérieur : elle semble inoffensive jusqu’à ce que vous réalisiez qu’elle demande une attention constante et attire inexplicablement des bugs.
Faits et contexte historique pour vos arguments
Ce ne sont pas des anecdotes pour anecdoter. Ce sont des faits qui vous aident à gagner des revues de design et à empêcher les équipes de copier-coller une librairie lourde « parce que tout le monde le fait ».
<details>est un élément HTML réel. Ce n’est pas un div avec une bonne impression ; c’est un widget de divulgation standardisé avec basculement intégré et un mapping d’accessibilité défini dans les navigateurs modernes.- Le comportement des navigateurs était incohérent au départ. Pendant des années, certains moteurs géraient le focus/le clavier de
<summary>différemment ; beaucoup d’équipes ont écrit des polyfills. Aujourd’hui, c’est largement stable, mais des hypothèses héritées subsistent dans du code ancien. - Le motif « accordéon » est plus ancien que les apps web. Les UI desktop avaient des triangles de divulgation et des panneaux extensibles bien avant les frameworks SPA ; le web reprend le même modèle mental, maintenant avec des sémantiques.
- Onglets et accordéons ont des rôles différents. Les onglets impliquent une vue « courante » unique parmi des pairs ; les accordéons impliquent des panneaux extensibles multiples. Les utilisateurs les interprètent différemment, et les lecteurs d’écran les annoncent différemment.
- L’amélioration progressive prédates les frameworks modernes. Elle est née de la réalité du web hétérogène : réseaux lents, support partiel et défaillances sont la norme, pas des cas marginaux.
- ARIA ne remplace pas la sémantique. ARIA peut décrire un comportement, mais si vous pouvez utiliser un élément natif, faites-le généralement. ARIA est tranchante ; elle coupe dans les deux sens.
- Les onglets fournis par les frameworks se cassent souvent quand l’hydratation est tardive. Le serveur rend « Onglet A », le client hydrate « Onglet B », et vous obtenez un scintillement plus un décalage d’état. La divulgation native évite beaucoup de ça parce que le navigateur gère l’état de base.
- Le deep-linking est une exigence récurrente. Les équipes produit adorent « partager un lien vers le troisième panneau ». Si vous l’ignorez, quelqu’un ajoutera plus tard une logique de hash et créera une nouvelle classe de bugs.
Accordéons avec details/summary : le choix par défaut
Utilisez <details> quand vous avez un titre qui bascule la visibilité d’un contenu. C’est tout. Si l’interaction est « cliquer le titre pour afficher/masquer le corps », ne négociez pas : commencez par <details>.
HTML de base qui fonctionne partout où le contenu fonctionne
Cette base ne dépend ni du CSS ni du JavaScript. Si les styles échouent, le contenu reste linéaire et lisible. Si les scripts échouent, la divulgation s’ouvre et se ferme toujours.
cr0x@server:~$ cat accordion.html
<section aria-label="Shipping FAQs">
<h2>Shipping</h2>
<details>
<summary>When do you ship?</summary>
<p>Orders placed before 2pm ship the same business day.</p>
</details>
<details>
<summary>Do you ship internationally?</summary>
<p>Yes. Duties and taxes are calculated at checkout when available.</p>
</details>
<details open>
<summary>How do returns work?</summary>
<p>Start a return within 30 days. We email a label if eligible.</p>
</details>
</section>
Remarquez l’attribut open. Ce n’est pas qu’esthétique : c’est votre état par défaut rendu côté serveur, et c’est une échappatoire pratique pour « premier élément ouvert par défaut » sans script.
Styliser details/summary sans le casser
La façon la plus rapide de ruiner <details> est d’enlever le marqueur de <summary>, supprimer son contour de focus, puis le remplacer par un div parce que « design ». Ne le faites pas. Vous pouvez le styliser et le garder opérable.
- Conservez un indicateur de focus visible sur
summary. - Si vous personnalisez le marqueur, faites-le en CSS, pas en supprimant la sémantique.
- N’insérez pas de contrôles interactifs à l’intérieur de
summarysauf si vous avez testé le comportement clavier de manière approfondie (spoiler : ça devient étrange).
Comportement « une seule ouverture à la fois » (optionnel)
<details> natif n’impose pas la règle « une seule ouverture ». C’est une bonne chose : il ne présume pas votre UX. Si vos designers insistent sur le comportement classique (ouvrir un panneau ferme les autres), améliorez-le avec un petit script qui écoute toggle.
Principe clé : si le script échoue, les utilisateurs peuvent toujours ouvrir plusieurs panneaux. C’est un repli acceptable.
cr0x@server:~$ cat accordion-one-open.js
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("[data-accordion]").forEach((root) => {
const items = Array.from(root.querySelectorAll("details"));
root.addEventListener("toggle", (e) => {
const target = e.target;
if (!(target instanceof HTMLDetailsElement)) return;
if (!target.open) return;
for (const item of items) {
if (item !== target) item.open = false;
}
});
});
});
Attachez-le ainsi :
cr0x@server:~$ cat accordion-enhanced.html
<section data-accordion aria-label="Billing FAQs">
<h2>Billing</h2>
<details>
<summary>Can I get an invoice?</summary>
<p>Invoices are available in your account within 24 hours.</p>
</details>
<details>
<summary>Do you support ACH?</summary>
<p>Yes for annual plans; contact support to enable it.</p>
</details>
</section>
C’est de l’amélioration progressive bien faite : le comportement de base est natif ; le comportement enrichi est additif ; le mode défaillant est acceptable.
Améliorations progressives qui ne sabotent pas la résilience
Les améliorations sont l’endroit où les équipes reconstruisent accidentellement une bibliothèque UI, puis s’étonnent qu’elle se comporte comme telle. Si vous améliorez <details>, conservez la forme du DOM et l’interaction native. Ne vous battez pas contre l’élément.
Amélioration 1 : ouverture/fermeture animée sans saccades
Animer la hauteur est le classique piège à pieds. Cela déclenche des layouts, peut être saccadé sous charge, et a tendance à casser quand le contenu est dynamique. Si vous devez animer, privilégiez l’animation de l’opacité et d’une petite transformation, ou utilisez la nouvelle approche CSS avec content-visibility pour les grands panneaux. Restez subtil.
Aussi : n’animez pas d’une manière qui retarde le contenu pour les aides techniques. L’animation est du vernis visuel ; l’accessibilité est le plat principal.
Amélioration 2 : deep-link vers un panneau spécifique
Quand quelqu’un partage « voir la troisième question », il veut une URL stable qui ouvre le bon panneau. Faites-le avec des IDs et de la logique de fragment.
- Donnez à chaque
<details>unidstable. - Au chargement, si
location.hashcorrespond à undetailsID, ouvrez-le et scrollez-le en vue.
cr0x@server:~$ cat accordion-deeplink.js
document.addEventListener("DOMContentLoaded", () => {
const id = decodeURIComponent(location.hash.replace(/^#/, ""));
if (!id) return;
const el = document.getElementById(id);
if (el && el.tagName === "DETAILS") {
el.open = true;
el.scrollIntoView({ block: "start" });
el.querySelector("summary")?.focus();
}
});
Mode défaillant si ce script ne se charge pas : la page fonctionne toujours ; le hash ne s’ouvre juste pas automatiquement. C’est tolérable.
Amélioration 3 : analytics sans transformer l’UI en moteur de télémétrie
On vous demandera de logger quels panneaux les utilisateurs ouvrent. Très bien. Utilisez l’événement natif toggle ; n’attachez pas de gestionnaires click sur summary qui écrasent le comportement par défaut. Votre rôle est d’observer, pas de prendre le volant.
cr0x@server:~$ cat accordion-analytics.js
document.addEventListener("DOMContentLoaded", () => {
document.body.addEventListener("toggle", (e) => {
const d = e.target;
if (!(d instanceof HTMLDetailsElement)) return;
if (!d.id) return;
const payload = { id: d.id, open: d.open, ts: Date.now() };
navigator.sendBeacon?.("/ui/toggle", JSON.stringify(payload));
}, true);
});
Si sendBeacon n’est pas disponible, rien ne casse. Vous perdez des analytics, pas l’expérience utilisateur.
Amélioration 4 : impression et « absence de CSS »
Les styles d’impression comptent plus que vous ne le pensez, car « imprimer » signifie souvent « sauvegarder en PDF », et « sauvegarder en PDF » signifie souvent « joindre à un ticket de conformité ». Assurez-vous que les panneaux ouverts s’impriment développés, ou choisissez la règle « imprimer tout développé ».
Un motif pragmatique : dans le CSS d’impression, forcez tous les details en ouvert.
Onglets : quand details est inadapté, et comment faire
Les onglets ne sont pas des accordéons. Les onglets sont un widget de sélection unique : un onglet est actif, et son panneau est la vue courante. Cela compte pour les aides techniques, les conventions clavier et les attentes générales des utilisateurs. Essayer de simuler des onglets avec plusieurs <details> vous donne un widget multi-ouvert qui ressemble à des onglets mais se comporte comme une pile de bascules.
Alors quelle est la base pour des onglets, si on n’utilise pas une librairie ?
Base : une simple liste de liens
La base d’onglets la plus résiliente n’est… pas des onglets. C’est un ensemble de liens vers des sections de la page (ou vers des pages séparées). Cela charge vite, fonctionne partout et est trivialement deep-linkable.
cr0x@server:~$ cat tabs-baseline.html
<nav aria-label="Account sections">
<ul>
<li><a href="#profile">Profile</a></li>
<li><a href="#security">Security</a></li>
<li><a href="#billing">Billing</a></li>
</ul>
</nav>
<section id="profile">
<h2>Profile</h2>
<p>Update your name and contact details.</p>
</section>
<section id="security">
<h2>Security</h2>
<p>Manage sessions and multi-factor authentication.</p>
</section>
<section id="billing">
<h2>Billing</h2>
<p>Invoices, payment methods, and plan changes.</p>
</section>
C’est l’option « fonctionne dans un navigateur texte ». Et oui, en réalité d’entreprise, c’est parfois ce qui vous sauve : un bundle cassé ne devrait pas empêcher un client de trouver « Réinitialiser la MFA ».
Améliorer progressivement vers de vrais onglets (ARIA + JS minimal)
Lorsque vous avez vraiment besoin d’onglets — parce que le contenu est des panneaux pairs et que l’UI est dense — améliorez depuis la base. Conservez les mêmes sections avec des IDs. Ajoutez un tablist construit à partir de ces liens. Ensuite cachez/affichez les panneaux avec JavaScript. Si JS échoue, l’utilisateur a toujours les liens et les sections.
Profile content. In your real app, this would be real forms, not vibes.
Security content.
Billing content.
Et le script :
cr0x@server:~$ cat tabs.js
function activateTab(tab, tabs, panels, { focus = true } = {}) {
for (const t of tabs) t.setAttribute("aria-selected", String(t === tab));
for (const p of panels) p.hidden = (p.id !== tab.getAttribute("aria-controls"));
if (focus) tab.focus();
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll('[role="tablist"]').forEach((tablist) => {
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const panels = tabs
.map(t => document.getElementById(t.getAttribute("aria-controls")))
.filter(Boolean);
tablist.addEventListener("click", (e) => {
const tab = e.target.closest('[role="tab"]');
if (!tab) return;
activateTab(tab, tabs, panels);
history.replaceState(null, "", "#" + tab.id);
});
tablist.addEventListener("keydown", (e) => {
const current = document.activeElement.closest?.('[role="tab"]');
if (!current) return;
const i = tabs.indexOf(current);
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
e.preventDefault();
activateTab(tabs[(i + 1) % tabs.length], tabs, panels);
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
e.preventDefault();
activateTab(tabs[(i - 1 + tabs.length) % tabs.length], tabs, panels);
} else if (e.key === "Home") {
e.preventDefault();
activateTab(tabs[0], tabs, panels);
} else if (e.key === "End") {
e.preventDefault();
activateTab(tabs[tabs.length - 1], tabs, panels);
}
});
// Deep-link: open tab by #tab-id
const hash = decodeURIComponent(location.hash.replace(/^#/, ""));
if (hash) {
const t = document.getElementById(hash);
if (t && tabs.includes(t)) activateTab(t, tabs, panels, { focus: false });
}
});
});
C’est l’essentiel : état sélectionné, panneaux masqués, navigation clavier et deep-linking. Aucun framework requis. Aucun système réactif. Aucun « store d’onglets ».
Blague #2 : Si vous construisez des onglets avec un bus d’événements global, félicitations — vous avez inventé un grille-pain qui a besoin de Kubernetes.
Pourquoi pas des onglets purement CSS ?
Les onglets CSS-only utilisant des boutons radio peuvent marcher, mais ils sont souvent fragiles et maladroits pour le deep-linking, l’intégration de l’historique et la cohérence avec les aides techniques. Utilisez-les pour de petits widgets marketing si nécessaire. Pour l’UI produit, un script de 40 lignes est généralement plus robuste et bien plus facile à déboguer.
Accessibilité : garanties et tests
L’accessibilité n’est pas « ajouter de l’ARIA et c’est bon ». Il s’agit d’assurer que les utilisateurs peuvent opérer l’UI au clavier et avec les aides techniques, et que les annonces correspondent à ce qui se passe.
Pour details/summary
- Support clavier :
summarydoit être focalisable et basculable via le clavier. Les navigateurs modernes gèrent cela, à moins que vous ne le cassiez avec du style ou des gestionnaires d’événements. - Focus visible : ne supprimez jamais les outlines de focus sans fournir un remplacement clair.
- Zone cliquable : conservez
summarycomme principale cible tactile. - Contrôles interactifs imbriqués : évitez d’insérer boutons/liens dans
summary; si inévitable, testez à fond car click et toggle peuvent entrer en conflit.
Pour les onglets
- Rôles et relations :
tablistcontient destab; chaque ongletaria-controlsuntabpanel; chaque panneauaria-labelledbyl’onglet. - État sélectionné : un seul onglet devrait avoir
aria-selected="true". - Conventions clavier : les flèches déplacent entre onglets ; Home/End vont aux extrémités ; Entrée/Espace activent (manuel) ou pas (automatique) selon votre choix. Choisissez une option et soyez cohérent.
- Panneaux masqués : utilisez l’attribut
hiddenpour que les panneaux inactifs ne soient pas dans l’arbre d’accessibilité.
Opinion : Ne transformez pas un lien en role="tab" sauf si vous remplacez volontairement le comportement de navigation. Un onglet n’est pas un lien de navigation ; traitez-le comme un contrôle.
Ce qu’il faut tester en pratique
Tester l’accessibilité est moins mystique qu’on le prétend. Vous vérifiez des mécaniques prévisibles :
- La touche Tab déplace le focus vers les contrôles summary/onglet dans un ordre logique.
- Entrée/Espace basculent l’ouverture/fermeture de
<details>. - Les touches fléchées déplacent entre onglets ; le focus ne se perd pas dans du contenu masqué.
- Le lecteur d’écran annonce le type de contrôle et l’état (développé/replié ; onglet sélectionné).
Tâches pratiques : commandes, sorties et décisions
Voici les tâches à lancer quand un « simple accordéon » devient un problème en production. Chacune inclut une commande réaliste, ce que la sortie signifie et quelle décision prendre ensuite. C’est là que le front rencontre le SRE : on ne devine pas, on mesure.
Tâche 1 : Vérifier que le HTML contient bien details/summary (pas rendu client uniquement)
cr0x@server:~$ curl -sS https://app.example.internal/faq | grep -n "<details" | head
142:<details id="returns">
163:<details id="shipping">
Signification : La réponse serveur inclut déjà les widgets de divulgation. Bon point de départ. Décision : Vous pouvez compter sur l’amélioration progressive ; une panne JS ne videra pas la FAQ.
Tâche 2 : Détecter si une librairie UI est tirée juste pour l’accordéon/les onglets
cr0x@server:~$ curl -sS https://app.example.internal/faq | grep -Eo 'src="[^"]+\.js"' | head
src="/assets/runtime-8a1c.js"
src="/assets/vendor-2b19.js"
src="/assets/faq-41d0.js"
Signification : Il y a un bundle vendor sur la page FAQ. Décision : Auditez son contenu ; si c’est surtout le runtime d’un framework UI, envisagez de livrer du HTML plat pour cette page.
Tâche 3 : Quantifier la charge JS et les en-têtes de cache
cr0x@server:~$ curl -sSI https://app.example.internal/assets/vendor-2b19.js | sed -n '1,12p'
HTTP/2 200
content-type: application/javascript
cache-control: public, max-age=31536000, immutable
content-length: 286401
etag: "vendor-2b19"
Signification : ~280 KB pour le JS vendor, cacheable long-terme. Pas catastrophique, mais beaucoup pour une « FAQ accordion ». Décision : Si cette page est à fort trafic ou utile en incident, retirez le JS inutile et gardez-la statique.
Tâche 4 : Identifier si le comportement d’accordéon est du JS personnalisé qui écrase le natif
cr0x@server:~$ rg -n "preventDefault\\(\\)" public/assets/faq-41d0.js | head
1187:e.preventDefault();
Signification : Le script empêche le comportement par défaut de quelque chose — probablement le click sur summary. Décision : Inspectez le handler ; supprimez la prévention par défaut sauf si c’est absolument nécessaire.
Tâche 5 : Confirmer que la page fonctionne sans JavaScript (vérification headless)
cr0x@server:~$ chromium --headless --disable-gpu --dump-dom https://app.example.internal/faq | grep -n "<summary" | head
143:<summary>How do returns work?</summary>
164:<summary>Do you ship internationally?</summary>
Signification : Les summaries sont présents dans le dump DOM. Décision : Si l’interaction échoue uniquement quand JS est activé, le bug se situe dans le code d’amélioration, pas dans la base.
Tâche 6 : Vérifier les IDs dupliqués qui cassent le deep-linking
cr0x@server:~$ chromium --headless --disable-gpu --dump-dom https://app.example.internal/faq | grep -o 'id="[^"]*"' | sort | uniq -d | head
id="shipping"
Signification : Un id="shipping" dupliqué existe. Décision : Corrigez les templates pour garantir des IDs uniques ; le deep-linking et les relations de labels en dépendent.
Tâche 7 : Détecter les lectures de layout dans le script d’amélioration (indice de profiling)
cr0x@server:~$ rg -n "getBoundingClientRect\\(|offsetHeight|scrollHeight" tabs.js accordion-one-open.js
Signification : Aucune lecture directe de layout dans ces petits scripts. Décision : Si vous voyez ces appels dans des boucles ou des handlers toggle, attendez-vous à des saccades ; réécrivez la logique d’animation.
Tâche 8 : Confirmer que la CSP ne bloque pas vos petits scripts d’amélioration
cr0x@server:~$ curl -sSI https://app.example.internal/faq | grep -i content-security-policy
content-security-policy: default-src 'self'; script-src 'self'; object-src 'none'
Signification : Les scripts inline sont probablement bloqués ; les scripts externes depuis self sont autorisés. Décision : Déployez les améliorations comme fichiers statiques, pas comme blobs <script> inline.
Tâche 9 : Vérifier que les panneaux d’onglets sont bien exclus de l’arbre d’accessibilité
cr0x@server:~$ chromium --headless --disable-gpu --dump-dom https://app.example.internal/account | grep -n 'role="tabpanel"' | head
88:<div role="tabpanel" id="panel-profile" aria-labelledby="tab-profile">
92:<div role="tabpanel" id="panel-security" aria-labelledby="tab-security" hidden>
Signification : Les panneaux non actifs sont hidden. Décision : Continuez à utiliser hidden plutôt que du masquage CSS pour les tabpanels.
Tâche 10 : Trouver des écouteurs d’événements qui pourraient se multiplier (double-binding au rerender)
cr0x@server:~$ rg -n "addEventListener\\(\"click\"|addEventListener\\(\"toggle\"" public/assets/*.js | head
public/assets/faq-41d0.js:221:addEventListener("click", function(e){
public/assets/faq-41d0.js:489:addEventListener("click", function(e){
Signification : Multiples listeners click dans le même bundle. Pas automatiquement mauvais, mais suspect sur une page simple. Décision : Assurez-vous que les écouteurs sont délégués une seule fois par conteneur, pas réattachés par item à chaque rendu.
Tâche 11 : Mesurer le TTFB et le temps de téléchargement du contenu pour séparer réseau vs JS
cr0x@server:~$ curl -o /dev/null -sS -w 'ttfb=%{time_starttransfer} total=%{time_total} size=%{size_download}\n' https://app.example.internal/faq
ttfb=0.084531 total=0.129774 size=40218
Signification : Le serveur est rapide ; le réseau n’est pas le problème. Décision : Si l’UX est lente, concentrez-vous sur les assets bloquants le rendu et le JS main-thread.
Tâche 12 : Vérifier si le script d’amélioration bloque le rendu
cr0x@server:~$ curl -sS https://app.example.internal/faq | grep -n "<script" | head
35:<script src="/assets/vendor-2b19.js"></script>
36:<script src="/assets/faq-41d0.js"></script>
Signification : Les scripts sont chargés sans defer ni type="module". Ils bloquent le parsing. Décision : Ajoutez defer aux scripts non critiques, surtout aux scripts d’amélioration seulement.
Tâche 13 : Valider la compression gzip/brotli du JS
cr0x@server:~$ curl -sSI -H 'Accept-Encoding: br' https://app.example.internal/assets/vendor-2b19.js | grep -iE 'content-encoding|content-length'
content-encoding: br
content-length: 68421
Signification : Brotli réduit significativement la taille transférée. Décision : Si la compression manque, corrigez la configuration CDN/serveur avant de discuter des micro-optimisations.
Tâche 14 : Confirmer que le comportement « une seule ouverture » n’est pas forcé par des hacks CSS
cr0x@server:~$ rg -n "details\\[open\\].*~.*details\\[open\\]" public/assets/*.css | head
Signification : Aucun hack sibling CSS trouvé. Décision : Utilisez le petit script JS ; le CSS ne peut pas garantir « une seule ouverture » de manière fiable sur des layouts arbitraires.
Procédure de diagnostic rapide
Quand des onglets ou accordéons « parfois ne fonctionnent pas », la pire réaction est de regarder d’abord le code. Diagnostiquez comme un opérateur : réduisez la classe de défaillance en quelques minutes.
Première étape : le HTML de base est-il correct ?
- Vérifier : Affichez la source (pas l’inspecteur) et confirmez que
<details>/<summary>existent, ou que le contenu d’onglet existe comme sections normales. - Si absent : Vous faites un rendu client-only. Décidez si c’est acceptable pour ce composant. Pour les FAQs et l’aide, généralement non.
Deuxième étape : le JavaScript bloque-t-il l’interaction par accident ?
- Vérifier : Cherchez
preventDefault()sur les clicks summary, des handlers click globaux, ou des overlays qui interceptent les clics. - Si présent : Supprimez/limitez le handler. Le
<details>natif ne devrait pas nécessiter de plomberie click.
Troisième étape : est-ce un problème d’état, d’hydratation ou de duplication ?
- Vérifier : IDs dupliqués, double attachement d’événements, et mismatch serveur/client sur l’état initial ouvert/sélectionné.
- Si mismatch : Faites sortir l’état initial canonique côté serveur (par ex. ajouter l’attribut
open; sélectionner le premier onglet) et laissez le client améliorer sans réécrire l’historique.
Quatrième étape : le goulot d’étranglement est-il performance ou correction ?
- Vérifier : Les interactions sont-elles retardées (main-thread occupé) ou cassées (pas de basculement) ?
- Si retardées : Profilez les tâches longues ; retirez le code lourd des pages de divulgation ; defer les scripts.
- Si cassées : Concentrez-vous d’abord sur la gestion des événements et la structure DOM ; les correctifs de performance ne répareront pas des sémantiques incorrectes.
Erreurs courantes : symptômes → cause → correctif
Cette section existe parce que ces bugs réapparaissent dans les équipes comme la grippe saisonnière, sauf que le remède est de la discipline.
1) « L’accordéon ne s’ouvre pas sur certains appareils »
Symptôme : Cliquer sur le summary ne fait rien, ou ouvre puis se referme immédiatement.
Cause racine : Un handler click sur summary appelle preventDefault() ou bascule open deux fois (une fois par le navigateur, une fois par le script). Souvent introduit par de l’analytics ou une « animation personnalisée ».
Correctif : Enlevez la prévention par défaut ; écoutez toggle sur details à la place. Si vous avez besoin d’un contrôle manuel, acceptez que vous construisez un composant personnalisé (et testez-le comme tel).
2) « Les utilisateurs clavier ne peuvent pas opérer l’accordéon »
Symptôme : La touche Tab ignore les summaries, ou le focus est invisible.
Cause racine : Le CSS a supprimé les outlines ; l’affichage de summary a été modifié d’une manière qui casse le focus ; summary a été remplacé par un div.
Correctif : Gardez un vrai <summary>. Restaurez les styles de focus. Testez au clavier avant de merger.
3) « Les deep links ouvrent le mauvais panneau »
Symptôme : Le fragment URL pointe vers un panneau, mais un autre s’ouvre, ou le scroll saute de manière erratique.
Cause racine : IDs dupliqués ou scripts qui réécrivent location.hash au chargement sans vérifier la cible.
Correctif : Garantie d’IDs uniques ; n’appelez history.replaceState que sur action utilisateur explicite ; validez les cibles du hash.
4) « Les onglets scintillent au chargement »
Symptôme : Tous les panneaux s’affichent brièvement puis un seul se cache ; ou l’onglet actif change après un instant.
Cause racine : Le rendu de base affiche toutes les sections ; l’amélioration JS applique le masquage après le layout ; mismatch d’hydratation ou script chargé tardivement.
Correctif : Rendre l’état initial « actif seulement » côté serveur si possible (ex. ajouter hidden) ou appliquer un petit basculement de classe avant le paint — si la CSP le permet — ou utiliser defer plus un CSS qui cache les panneaux seulement quand la classe « JS-enabled » est présente.
5) « Le comportement “une seule ouverture” se ferme de façon imprévisible »
Symptôme : Ouvrir un panneau le referme immédiatement, ou l’ouverture d’un panneau ferme un accordéon différent ailleurs.
Cause racine : La délégation d’événements est attachée à document et non scoped ; le handler collecte les details de toute la page.
Correctif : Scopez au conteneur avec data-accordion ; fermez uniquement les siblings dans ce conteneur.
6) « Le lecteur d’écran annonce des rôles étranges ou répète les titres »
Symptôme : Les tabpanels sont annoncés même quand ils sont masqués ; les onglets annoncés comme liens ; labels répétés.
Cause racine : Masquage via CSS seulement ; relations ARIA incorrectes ; utilisation incohérente de display:none ; réutilisation d’IDs entre plusieurs tablists.
Correctif : Utilisez hidden pour les tabpanels inactifs ; imposez des IDs uniques par instance de tab ; validez les paires aria-controls et aria-labelledby.
Trois mini-histoires d’entreprise (anonymisées)
Incident : une mauvaise hypothèse sur « le JS charge toujours »
Un SaaS B2B de taille moyenne avait une page « Récupération de compte » avec des onglets : « Email », « Authentificator », « SSO fallback ». L’équipe produit voulait du sleek, donc ils ont utilisé un composant du bundle principal. Tout fonctionnait en staging, sur le Wi‑Fi du bureau, en dev local où tout est instantané et où personne n’a cinq VPN d’activés.
Puis un réseau client a commencé à bloquer un domaine tiers utilisé par un script analytics non lié. Le navigateur a attendu, réessayé, et retardé l’exécution du bundle principal d’une manière variable selon les appareils. Pour une partie des utilisateurs, l’UI des onglets a chargé trop tard et le HTML de base — essentiellement des espaces réservés vides — était tout ce qu’ils avaient. Les options de récupération étaient invisibles. Des gens étaient bloqués et furieux, ce qui est un ticket particulier à recevoir.
L’hypothèse erronée n’était pas « analytics pourrait être bloqué ». Tout le monde le sait. L’hypothèse erronée était que l’interaction critique pouvait être client-only. La correction n’a pas été héroïque : rendre côté serveur le contenu de récupération comme sections normales avec navigation par ancres, puis n’améliorer en onglets que si le JS charge. La fois suivante qu’un proxy d’entreprise fit des siennes, la récupération fonctionnait toujours. Personne n’a crié au on-call.
Optimisation qui s’est retournée contre eux : onglets CSS-only avec radios
Une équipe commerce a décidé d’enlever le JavaScript d’un widget d’onglets fiche produit (« Description », « Specs », « Warranty »). Bonne intention. Malheureusement, ils ont choisi l’astuce des radios CSS, puis l’ont mis dans un partial réutilisé sur tout le site — y compris une vue de comparaison où 20 produits sont renderés sur la même page.
Les inputs radio nécessitent des groupes name uniques et des IDs uniques pour que les labels restent liés au bon input. Le partial utilisait des IDs statiques parce que « c’est juste un composant ». Sur une page produit seul, ça allait. Dans la vue comparaison, cliquer « Specs » sur le produit A basculait le panneau « Warranty » du produit B. Les utilisateurs ont pensé que la page était hantée, ce qui est techniquement exact.
La correction a été d’arrêter de prétendre que « CSS-only » rime avec absence de complexité. Ils sont passés à une base par ancres (chaque section avait un ID), et une petite amélioration scoped qui upmode un seul conteneur en onglets. Les IDs ont été namespacés par identifiant produit. L’« optimisation » est devenue une vraie amélioration : moins de JS qu’avec le widget framework, plus de correction que le hack CSS.
Pratique ennuyeuse mais correcte qui a sauvé la mise : scoping et valeurs par défaut
Dans une firme financière, une revue sécurité a imposé une CSP stricte. Les scripts inline étaient hors-jeu. Certaines équipes ont paniqué parce que leurs composants UI dépendaient d’inline scripts par page pour initialiser des widgets. Une équipe n’a pas paniqué, car leurs patterns d’interaction étaient construits sur l’amélioration progressive avec des scripts externes et un scoping strict.
Leurs pages riches en divulgations utilisaient le natif <details>. Les améliorations (comportement une-seule-ouverture, ouverture par deep-link, analytics) étaient livrées comme fichiers statiques versionnés avec defer. L’initialisation était basée sur le conteneur : chaque accordéon avait data-accordion, chaque tablist était locale, et les écouteurs étaient délégués au sein du composant.
Quand la CSP est arrivée, ces pages ont continué de fonctionner. Pas de demande d’exception d’urgence. Pas d’en-têtes relaxés « temporairement » qui deviennent permanents. Juste un deploy vert et ennuyeux. En environnement corporate, l’ennuyeux est une fonctionnalité qu’il faut préserver.
Checklists / plan étape par étape
Étape par étape : livrer un accordéon de façon résiliente
- Commencez par le HTML : Utilisez
<details>/<summary>. Mettez du contenu réel à l’intérieur. Pas d’espaces réservés comme seule vérité. - Décidez l’état par défaut : Ajoutez
opensur le panneau que vous voulez développé par défaut (ou aucun). - Stylisez avec soin : Gardez le focus visible ; ne retirez pas la sémantique ; évitez les contrôles interactifs dans
summary. - Ajoutez des améliorations uniquement si demandé : comportement une-seule-ouverture, ouverture deep-link, analytics. Gardez chaque amélioration indépendante.
- Defer les scripts : Ajoutez
deferet gardez les scripts d’amélioration petits et locaux. - Testez les modes défaillants : JS désactivé, CSS désactivé (ou bloqué), réseau lent. Confirmez que le contenu reste accessible.
- Verrouillez les IDs : Assurez des IDs uniques et stables si vous avez besoin de deep links ou d’analytics.
Étape par étape : transformer une navigation par ancres en onglets
- Sections de base : Utilisez
<section id="..."><h2>...</h2>pour chaque panneau. - Navigation de base : Fournissez un
<nav>avec une liste de liens vers ces IDs. - Couche d’amélioration : Remplacez/augmentez la nav par un
role="tablist"de boutons quand JS est disponible. - Masquer les panneaux correctement : Utilisez
hiddensur les panneaux inactifs, pas uniquement du CSS. - Support clavier : Flèches, Home/End. Ne zappez pas ; cela fait partie du widget.
- Deep-linking : Utilisez le hash pour sélectionner un onglet. Ne poussez pas l’historique au chargement ; remplacez l’état sur action utilisateur.
- Scopez tout : Un widget d’onglets ne doit pas affecter un autre. Évitez les sélecteurs globaux qui attrapent tous les panneaux de la page.
Checklist de release pour systèmes de production
- Un utilisateur peut-il atteindre le contenu avec JS bloqué ?
- Un utilisateur peut-il opérer le contrôle uniquement au clavier ?
- Les liens hash ouvrent-ils le bon panneau/onglet ?
- Les IDs sont-ils uniques dans la page ?
- Les scripts sont-ils différés et cacheables ?
- Le composant fonctionne-t-il si l’analytics échoue ?
- Avez-vous testé au moins un profil « 3G lent / haute latence » ?
- Le code d’amélioration est-il scoped au conteneur (pas de couplage accidentel global) ?
FAQ
1) Dois-je toujours utiliser details/summary pour les accordéons ?
Oui pour le contenu typique de divulgation. Si vous avez besoin d’une coordination d’état complexe, d’en-têtes interactifs imbriqués ou d’une sémantique personnalisée, vous pourriez construire un composant custom — mais vous choisissez plus de tests et plus de risques.
2) Puis-je styliser le marqueur de summary ?
Oui, mais soyez prudent. Si vous retirez le marqueur par défaut, vous devez remplacer l’affordance (un indicateur clair) et préserver la visibilité du focus clavier. Ne cachez pas la seule indication qu’un élément est extensible.
3) Pourquoi ne pas utiliser le composant accordéon d’une librairie ?
Parfois vous devriez — si vous dépendez déjà de la librairie et que le composant est fortement personnalisé. Mais si vous rapatriez la librairie principalement pour des divulgations, vous payez une taxe récurrente : taille du bundle, bugs d’hydratation et churn de dépendances.
4) Les onglets sont-ils possibles sans JavaScript ?
Pas en tant que « vrais onglets » avec la sémantique d’onglet et les conventions clavier. La base devrait être une navigation par ancres ou des pages séparées. Ensuite, améliorez avec JS pour obtenir des onglets si nécessaire.
5) Les boutons d’onglet doivent-ils être des liens ?
Généralement non. Les onglets sont des contrôles ; utilisez <button> avec role="tab". Si vous voulez un comportement de navigation, utilisez des liens et ne parlez pas d’onglets. Mélanger les deux crée un comportement déroutant pour les utilisateurs et les aides techniques.
6) Comment gérer les deep links pour les onglets ?
Utilisez le hash pour représenter la sélection d’onglet (ex. #tab-security). Au chargement, lisez le hash et activez l’onglet correspondant. Au clic utilisateur, mettez à jour le hash avec history.replaceState pour éviter de polluer l’historique du navigateur.
7) Quelle est la manière la plus sûre d’imposer « une seule ouverture » dans un accordéon ?
Écoutez l’événement toggle sur un conteneur et fermez les éléments details siblings quand l’un s’ouvre. Scopez cela à un conteneur d’accordéon pour ne pas fermer des divulgations non liées ailleurs.
8) Puis-je imbriquer des éléments details ?
Vous pouvez, mais il est facile de créer des interactions et des chemins de focus déroutants. Si vous imbriquez, gardez les summaries simples, évitez les contrôles imbriqués dans les en-têtes et testez intensément la navigation clavier.
9) Comment éviter le flicker lors de l’amélioration en onglets ?
Rendez l’état initial « uniquement actif » côté serveur en utilisant hidden, ou appliquez une classe « js-enabled » tôt et ne cachez les panneaux que quand cette classe existe. Ne comptez pas sur un JS chargé tardivement pour cacher le contenu après layout.
10) Quel est le plan de test minimum avant mise en production ?
Opération au clavier uniquement, accessibilité avec JS désactivé, validation d’IDs uniques, et au moins un profil réseau lent. Si vous ne pouvez pas faire cela, vous livrez de l’espoir plutôt qu’un logiciel.
Conclusion : prochaines étapes qui ne vous hanteront pas
Si vous retenez une chose : partez d’un HTML qui fonctionne déjà. <details>/<summary> est votre ami pour les divulgations. Les onglets nécessitent du JavaScript, mais pas forcément un framework — et ils n’exigent absolument pas du contenu client-only.
Prochaines étapes pratiques :
- Choisissez une page accordéon à fort trafic (FAQ, tarification, aide paramètre). Remplacez l’accordéon de la librairie par du
<details>natif et mesurez la réduction du bundle. - Pour les widgets d’onglets existants, assurez-vous d’une navigation de base par ancres et de sections. Puis remettez les onglets en tant qu’amélioration avec du JS scoped.
- Exécutez les tâches de diagnostic ci‑dessus en CI : vérifiez les IDs dupliqués et les scripts bloquants le rendu sur les pages riches en contenu.
- Rédigez le « contrat composant » de votre équipe : comportement de base sans JS, comportement enrichi avec JS, et modes de défaillance acceptables.
Faites cela, et votre UI ne se limitera pas à être jolie. Elle continuera de fonctionner quand le monde réel montrera son nez, ce que font réellement les systèmes en production.