Organiser l’interface avec des panneaux latéraux dynamiques
De quoi parle-t-on réellement ? Dans nos interfaces, on croise très souvent des panneaux qui viennent se glisser depuis un bord de l’écran. On les voit par exemple dans la barre latérale de Google Maps, ou dans les panneaux d’outils d’interfaces comme celles proposées dans certains dashboards. On les déclenche pour afficher un menu, des options, un formulaire ou des informations secondaires, puis on les referme pour revenir au contenu principal. Selon les contextes, on parle de off-canvas, de drawer, de sidebar ou encore de panel. Les termes changent, mais le fonctionnement reste le même : un bloc est placé en dehors de la zone visible, puis il entre dans l’écran à la demande de l’utilisateur.
Ce mécanisme répond à un besoin très concret : ne pas surcharger l’interface tout en gardant des fonctionnalités accessibles. Sur mobile comme sur desktop, il permet de structurer l’écran sans multiplier les zones visibles en permanence. On n’est donc pas face à un simple effet d’animation, mais bien à une manière d’organiser l’interface en fonction de l’usage.
Que va couvrir cet article ?
L’objectif n’est pas ici de faire un tour complet des solutions existantes ni de s’appuyer sur un framework. Nous allons partir d’un cas simple, en CSS et JavaScript natifs, pour comprendre comment ce type de composant fonctionne réellement. Nous allons mettre en place un panneau basique, puis faire évoluer progressivement son comportement : ouverture, fermeture, positionnement, paramètres. L’idée est de comprendre ce que l’on fait, pas seulement d’obtenir un résultat.
Nous verrons également comment distinguer la mécanique du composant (JS et CSS fonctionnels liés à l’ouverture, la fermeture, les états et les transitions) de ce qui relève de l’habillage (styles visuels, animations décoratives, interactions spécifiques au projet). L’objectif est de conserver un noyau réutilisable, indépendant du contexte graphique, que l’on peut intégrer et adapter facilement sans réécrire la logique de base.
La mécanique du composant
Concrètement, un panneau latéral est un bloc HTML placé en dehors de la zone visible, que l’on fait entrer à l’écran lors d’une interaction utilisateur. Ce bloc joue le rôle de conteneur et s’organise simplement autour de deux éléments : une zone de contenu, qui accueille les informations ou les outils, et un élément de pilotage, que nous appellerons dans la suite de l’article le grip. C’est ce grip qui permet d’ouvrir et de refermer le panneau, qu’il prenne la forme d’une icône, d’un bouton, d’une flèche ou d’une zone interactive dédiée.
<body>
<div id="panneau-lateral-options" class="panneau-lateral">
<button type="button" class="panneau-lateral__grip">
Options
</button>
<div class="panneau-lateral__contenu">
<h2>Paramètres du panneau</h2>
<p>Ce bloc accueillera les options, les filtres ou les outils associés à l’interface.</p>
</div>
</div>
</body>La première question concerne la balise principale, <div> ou pas <div> ? Il n’existe pas de réponse unique : tout dépend du contenu. Si le panneau contient une navigation, une balise <nav> est cohérente. Pour un contenu secondaire, on pourra utiliser <aside>. Si le bloc a une logique propre avec un titre, <section> peut être pertinent. Dans les autres cas, une simple <div> reste acceptable. La sémantique dépend du rôle du contenu, pas de son comportement.
Côté mécanique, le principe est toujours le même. Le panneau est positionné hors écran via le CSS, puis déplacé dans la zone visible lorsqu’un état change. Ce changement peut être déclenché par un clic sur un bouton, une icône ou une zone de préhension. On a donc trois éléments essentiels : un bloc masqué, un élément déclencheur, et un changement d’état. Tant que ces trois éléments sont clairement identifiés, le composant reste simple à comprendre et à faire évoluer.

À ce stade, on ne cherche pas à styliser ni à enrichir l’interaction. L’objectif est uniquement de mettre en place ce déplacement de base, pour disposer d’un point d’appui solide avant d’ajouter des variantes ou des comportements plus avancés.
États et interactions utilisateur
Avant d’aller plus loin, il est utile de poser clairement les états du composant. Un panneau latéral fonctionne toujours autour de deux situations principales : fermé et ouvert. Entre les deux, il existe un état intermédiaire, celui de la transition, pendant lequel le panneau se déplace. Ces états ne sont pas uniquement visuels, ils structurent le fonctionnement. Un panneau fermé ne doit pas capter d’interactions, un panneau ouvert doit être accessible, et la transition doit rester fluide sans bloquer l’interface.
L’interaction la plus simple reste le clic sur le grip. C’est lui qui va déclencher le changement d’état. Concrètement, cela revient à ajouter ou retirer une classe sur le conteneur principal, par exemple is-open, qui permettra au CSS de gérer le déplacement.
<div id="panneau-lateral-options" class="panneau-lateral">
<!-- état fermé par défaut -->
</div>
<div id="panneau-lateral-options" class="panneau-lateral is-open">
<!-- état ouvert -->
</div>Cette approche reste simple et lisible : le HTML porte l’état (via les classes), le CSS gère l’affichage (position, transitions, visibilité), et le JavaScript se limite aux déclencheurs (clic, interaction utilisateur). Chacun joue son rôle, sans chevauchement inutile..
const panneau = document.getElementById('panneau-lateral-options');
const grip = panneau.querySelector('.panneau-lateral__grip');
grip.addEventListener('click', () => {
panneau.classList.toggle('is-open');
});À ce stade, on ne cherche pas encore à gérer des cas complexes. Ces cas existent pourtant : gestion du focus clavier, fermeture au clic extérieur, empilement de plusieurs panneaux, interactions tactiles (glisser pour ouvrir ou fermer), ou encore chargement dynamique de contenu. Nous y reviendrons plus loin. L’objectif ici reste de poser une base claire : un composant avec des états identifiables, une interaction simple, et un comportement prévisible.
Voir le panneau en action
Avant d’aller plus loin, mettons simplement en place quelques règles CSS pour faire fonctionner notre panneau latéral, sans chercher à couvrir tous les cas. L’objectif est de valider visuellement le comportement : un panneau fermé, un clic sur le grip, une ouverture, puis une fermeture.
<div id="panneau-lateral-options" class="panneau-lateral">
<div class="panneau-lateral__contenu">
<p>Contenu du panneau</p>
</div>
<button type="button" class="panneau-lateral__grip">Grip</button>
</div>Ce premier bloc pose uniquement la structure : un conteneur, une zone de contenu, et le grip qui servira de déclencheur. On reste volontairement minimal pour se concentrer sur la mécanique.
.panneau-lateral {
position: fixed;
top: 0;
left: 0;
width: 300px;
height: 100%;
transform: translateX(-260px); /* 300px - 40px de grip visible */
transition: transform 0.3s ease;
background-color: #9fd8e4;
text-align: right; /* uniquement pour rendre le grip bien visible dans cet exemple */
}
.panneau-lateral.is-open {
transform: translateX(0);
}
.panneau-lateral__grip {
background: #d7d2ae;
cursor: pointer;
}Ici, quelques règles suffisent pour voir le panneau fonctionner. Certaines propriétés, comme text-align, sont utilisées uniquement pour rendre la démo lisible et positionner rapidement le grip. Elles ne font pas partie de la mécanique du composant. Dans un cas réel, on utiliserait un positionnement plus précis. L’objectif reste de garder un CSS minimal, centré uniquement sur le déplacement du panneau.
const panneau = document.getElementById('panneau-lateral-options');
const grip = panneau.querySelector('.panneau-lateral__grip');
grip.addEventListener('click', () => {
panneau.classList.toggle('is-open');
});Le JavaScript se limite ici au déclencheur : un clic qui ajoute ou retire une classe. Le reste est pris en charge par le HTML et le CSS. On obtient ainsi un comportement complet avec très peu de code. Le panneau reste partiellement visible grâce au grip, et l’interaction est immédiate. Ce point de passage est volontaire : il permet de valider la mécanique avant d’introduire des variantes plus structurées. Vous pouvez également consulter cette première mise en place en situation réelle via la démo associée : Offcanvas 01 bases, ou une version améliorée sur Offcanvas 02 bases.
Séparer mécanique et habillage dès le départ
Avant d’aller plus loin, il est important de poser une règle simple : nous allons volontairement séparer ce qui relève du fonctionnement du composant et ce qui relève de son rendu. Dans les exemples qui suivent, cela se traduit concrètement par :
- une partie mécanique, qui gère la position, les dimensions et les interactions
- une partie habillage, qui gère les couleurs, les espacements et l’organisation visuelle du contenu
Cette séparation n’est pas théorique. Elle permet de faire évoluer le composant sans tout casser : on peut changer l’apparence sans toucher au fonctionnement, ou inversement. Dans le code HTML de démonstration, nous avons également fait deux choix volontairement simples :
- aucun « vrai » grip n’est défini dans le HTML
- une zone visible est simulée via CSS (pseudo-élément)
L’interaction est déclenchée en cliquant sur le panneau lui-même, ce qui permet de se concentrer uniquement sur la mécanique. Un sélecteur centré dans la page permet de changer dynamiquement la position du panneau (gauche, haut, droite, bas) afin d’observer son comportement dans chaque cas, et de constater que ce sont seulement les CSS qui gère le panneau, sa position, ses états, son aspect…
Positionnement et comportement spatial
Nous partons ici d’un composant déjà fonctionnel : il s’ouvre et se ferme correctement. L’objectif est maintenant de comprendre comment ce même comportement s’adapte selon la position du panneau dans l’écran : à gauche, à droite, en haut ou en bas. Le panneau repose sur une classe de base, panneau-lateral, à laquelle on associe une classe de position. Cette classe indique à la fois le côté d’ancrage du panneau et le sens dans lequel il doit être masqué puis réaffiché.
<div id="panneau-lateral-options" class="panneau-lateral panneau-lateral--droite">
<!-- état fermé par défaut -->
</div>Le JavaScript ne fait qu’ajouter ou retirer des classes. Concrètement, il applique des modificateurs comme panneau-lateral--gauche, panneau-lateral--droite, panneau-lateral--haut ou panneau-lateral--bas. Ces classes décrivent uniquement la position souhaitée. Toute la logique de positionnement, de dimension et de déplacement associée à ces variantes va donc être portée portée par le CSS.
Définir les repères du composant
On commence par poser des repères simples, qui servent de base à tous les calculs. Ces valeurs sont définies sous forme de variables afin de faciliter leur adaptation aux principaux contextes (desktop, mobile, panneau compact ou étendu). Elles permettent de raisonner en dimensions réelles, sans valeurs arbitraires, tout en restant modulables.
Une fois le principe compris, ces variables peuvent être ajustées ou étendues : on peut par exemple limiter la hauteur d’un panneau latéral, modifier la largeur selon l’écran, jouer sur la vitesse de transition ou encore introduire des effets différents. L’objectif ici n’est pas de couvrir tous les cas, mais de poser une base claire, sur laquelle ces adaptations pourront s’appuyer.
:root {
--panneau-largeur: 300px; /* utilisé pour les panneaux gauche et droite */
--panneau-hauteur: 100px; /* utilisé pour les panneaux haut et bas */
--panneau-visible: 40px; /* partie visible du panneau (pseudo grip) */
/* --panneau-padding: 30px; optionnel : relève de l’habillage du contenu */
}Principe de déplacement
Le panneau est d’abord ancré à l’écran avec position: fixed, ce qui le rend indépendant du flux de la page. On le décale ensuite hors de la zone visible à l’aide de transform (vers la gauche, la droite, le haut ou le bas selon sa position). Lorsqu’il passe à l’état ouvert, on annule ce décalage pour le ramener exactement à son point d’ancrage initial.
.panneau-lateral {
position: fixed;
transition: transform 0.3s ease;
}
La propriété transform n’est pas définie sur la classe de base : elle s’adapte selon la position via les modificateurs.
--gauche/--droite→ déplacement horizontal avectranslateX(...)--haut/--bas→ déplacement vertical avectranslateY(...)
/*
Le point important ici est que le déplacement ne se fait pas en pourcentage,
mais à partir des dimensions du composant.
exemple avec une largeur donnée
*/
:root {
--panneau-largeur: 300px;
--panneau-visible: 40px;
}
/* fermé à gauche : on masque toute la largeur sauf la partie visible */
.panneau-lateral--gauche {
transform: translateX(calc(-1 * (300px - 40px))); /* = -260px */
}
/* ouvert : on annule le décalage */
.panneau-lateral.is-open {
transform: translateX(0);
}
/*
Dans cet exemple, le panneau mesure 300px de large et laisse apparaître 40px.
Il est donc décalé de 260px lorsqu’il est fermé, ce qui correspond exactement à sa largeur moins la zone visible.
*/Adapter dimensions selon la position
Pour rester cohérent, on applique la même logique dans les deux axes : la variable décrit la surface utile du panneau, et la zone visible est ajoutée à cette dimension. Ainsi, un panneau gauche ou droite mesure --panneau-largeur + --panneau-visible en largeur, tandis que sa hauteur reste à 100% de l’écran. À l’inverse, un panneau haut ou bas mesure --panneau-hauteur + --panneau-visible en hauteur, tandis que sa largeur occupe 100% de l’écran. Cette distinction est importante : 100% correspond à l’axe qui s’étire sur toute la fenêtre, alors que les variables définissent la dimension contrôlée du panneau.
.panneau-lateral--gauche,
.panneau-lateral--droite {
top: 0;
width: calc(var(--panneau-largeur) + var(--panneau-visible));
height: 100%;
}
.panneau-lateral--haut,
.panneau-lateral--bas {
left: 0;
width: 100%;
height: calc(var(--panneau-hauteur) + var(--panneau-visible));
}La distinction ne porte donc pas sur une différence de mécanique, mais uniquement sur l’axe utilisé. À gauche et à droite, on ajoute la zone visible à la largeur. En haut et en bas, on l’ajoute à la hauteur. Le déplacement, lui, ne concerne que la surface utile : --panneau-largeur sur l’axe horizontal, --panneau-hauteur sur l’axe vertical. Le détail des valeurs de transform et leur adaptation à chaque position (gauche, droite, haut, bas) est présenté dans le prochain sous-chapitre.
Unifier position et état
Plutôt que de multiplier les blocs par cas, on peut regrouper les règles essentielles dans un seul extrait. Chaque position ne définit que son point d’ancrage et son déplacement, tandis que l’état ouvert est centralisé.

/* gauche : ancrage à gauche, déplacement horizontal négatif */
.panneau-lateral--gauche {
left: 0;
transform: translateX(calc(-1 * (var(--panneau-largeur) - var(--panneau-visible))));
}
/* droite : ancrage à droite, déplacement horizontal positif */
.panneau-lateral--droite {
right: 0;
transform: translateX(calc(var(--panneau-largeur) - var(--panneau-visible)));
}
/* haut : ancrage en haut, déplacement vertical négatif */
.panneau-lateral--haut {
top: 0;
transform: translateY(calc(-1 * var(--panneau-hauteur)));
}
/* bas : ancrage en bas, déplacement vertical positif */
.panneau-lateral--bas {
bottom: 0;
transform: translateY(var(--panneau-hauteur));
}
/* ========================================================================================== */
/* état (unique pour toutes les positions) */
.panneau-lateral.is-open {
transform: translate(0, 0);
}Chaque règle reste volontairement minimale : un point d’ancrage et un transform. Le changement d’état (is-open) est commun, ce qui évite toute duplication et rend la logique plus lisible. On obtient ainsi un ensemble cohérent, facile à maintenir et à faire évoluer, sans multiplier les variantes, et cette approche permet de comprendre rapidement le comportement du panneau :
- on positionne le composant (left, right, top, bottom)
- on déplace le composant avec
transform - on réinitialise ce déplacement lorsqu’il est ouvert
Simuler la zone visible (pseudo grip)
Dans cette démonstration, aucun grip n’est défini dans le HTML. La zone visible est simulée en CSS via un pseudo-élément. L’objectif est de matérialiser la partie du panneau qui reste apparente lorsqu’il est fermé, sans ajouter de structure supplémentaire.
/* zones visibles (grip simulé) */
.panneau-lateral::after {
content: "";
position: absolute;
}
/* gauche : bande visible à droite du panneau */
.panneau-lateral--gauche::after {
right: 0; top: 0;
width: var(--panneau-visible);
height: 100%;
}
/* droite : bande visible à gauche du panneau */
.panneau-lateral--droite::after {
left: 0; top: 0;
width: var(--panneau-visible);
height: 100%;
}
/* haut : bande visible en bas du panneau */
.panneau-lateral--haut::after {
bottom: 0; left: 0;
width: 100%;
height: var(--panneau-visible);
}
/* bas : bande visible en haut du panneau */
.panneau-lateral--bas::after {
top: 0; left: 0;
width: 100%;
height: var(--panneau-visible);
}
Chaque variante positionne simplement cette bande visible sur le bon côté. Cette approche permet de visualiser le comportement du panneau sans introduire un vrai bouton ou un élément d’interface dédié.
Pourquoi cette approche ?
On ne cherche pas à masquer le panneau avec des artifices. On définit un espace réel, puis on déplace ce bloc. Le composant devient lisible, prévisible, et facile à adapter à d’autres contextes.
- la mécanique repose sur un changement d’état
- le positionnement est porté par des classes CSS
- les dimensions intègrent directement la zone visible
| Classe / état | Rôle | Effet attendu |
|---|---|---|
panneau-lateral |
Classe principale du composant. | Définit le panneau comme élément positionné, indépendant du flux de la page. |
panneau-lateral--gauche |
Modificateur de position. | Ancre le panneau à gauche et le masque vers la gauche lorsqu’il est fermé. |
panneau-lateral--droite |
Modificateur de position. | Ancre le panneau à droite et le masque vers la droite lorsqu’il est fermé. |
panneau-lateral--haut |
Modificateur de position. | Ancre le panneau en haut et le masque vers le haut lorsqu’il est fermé. |
panneau-lateral--bas |
Modificateur de position. | Ancre le panneau en bas et le masque vers le bas lorsqu’il est fermé. |
is-open |
Classe d’état. | Annule le décalage appliqué au panneau et le rend entièrement visible. |
Ce tableau résume les classes utilisées dans la démonstration. La classe principale définit le composant, les modificateurs indiquent sa position, et la classe d’état décrit son ouverture. La démo complète est visible en ligne sur Offcanvas 03 positionnement Haut, Droit, Bas ou Gauche.
Triple zone : icônes et libellés selon le contexte
Nous avons jusqu’ici travaillé avec un panneau capable de s’ouvrir, se fermer et se positionner. Dans un usage réel, un besoin apparaît rapidement : proposer plusieurs niveaux de lecture. Un panneau peut afficher uniquement des icônes, uniquement des libellés, ou les deux. Ce comportement ne modifie pas la mécanique du composant, mais agit sur son contenu et son encombrement. Nous introduisons donc une seconde dimension : le mode d’affichage du contenu. Avant toute chose, adaptons la structure HTML pour distinguer clairement les icônes et les labels.
<!-- Pour simplifier l'implémentation, nous utilisons des icones Bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<!-- Ensuite modifions la structure HTML afin de distinguer Icones et Labels séparément -->
<div id="panneau-lateral-options" class="panneau-lateral panneau-lateral--gauche">
<div class="panneau-lateral__contenu">
<div class="items"><i class="bi bi-1-square"></i><span class="label">Item 1</span></div>
<div class="items"><i class="bi bi-2-square"></i><span class="label">Item 2</span></div>
<div class="items"><i class="bi bi-3-square"></i><span class="label">Item 3</span></div>
<div class="items"><i class="bi bi-4-square"></i><span class="label">Item 4</span></div>
<div class="items"><i class="bi bi-5-square"></i><span class="label">Item 5</span></div>
<div class="items"><i class="bi bi-6-square"></i><span class="label">Item 6</span></div>
<div class="items"><i class="bi bi-7-square"></i><span class="label">Item 7</span></div>
</div>
</div>Coté largeur de panneau, seuls les modes Gauche et Droite doivent s’adapter à l’espace nécessaire. Une icône occupe moins de place qu’un label, et encore moins qu’un couple icône + label. Pour distinguer ces trois cas, on définit deux largeurs de référence, puis on les combine. Cette approche permet de contrôler précisément chaque mode tout en restant cohérent avec la logique du composant.
:root {
--panneau-largeur-icons: 100px; /* largeur en mode icônes */
--panneau-largeur-labels: 220px; /* largeur en mode labels */
--panneau-largeur: calc(var(--panneau-largeur-icons) + var(--panneau-largeur-labels)); /* mode complet */
}Activer les modes via des classes
Le mode d’affichage est piloté par une classe appliquée au conteneur #panneau-lateral-options. Aucun JavaScript complexe n’est requis : on ajoute ou retire simplement une classe, et tout le comportement est géré en CSS.
items-bothitems-iconsitems-labels
<div id="panneau-lateral-options" class="panneau-lateral panneau-lateral--gauche items-both">Important : la mécanique du panneau ne change pas. Seul l’habillage du contenu évolue.
/* Mode icônes : on masque les labels et on réduit la largeur */
.panneau-lateral.items-icons {
--panneau-largeur: var(--panneau-largeur-icons);
}
.panneau-lateral.items-icons .label {
display: none;
}
/* Mode labels : on masque les icônes et on adapte la largeur */
.panneau-lateral.items-labels {
--panneau-largeur: var(--panneau-largeur-labels);
}
.panneau-lateral.items-labels i {
display: none;
}
/* Mode complet : on affiche tout et on rétablit la largeur globale */
.panneau-lateral.items-both {
--panneau-largeur: 300px;
}
.panneau-lateral.items-both i,
.panneau-lateral.items-both .label {
display: inline-block;
}En mode icone seulement, le panneau peut être allégé. On réduit le padding, tout en conservant la contrainte du grip. On continue dans la logique d’adapter l’habillage sans toucher à la mécanique.
/* mode icônes : réduction du padding du panneau (gauche/droite uniquement) */
.panneau-lateral--gauche.items-icons .panneau-lateral__contenu,
.panneau-lateral--droite.items-icons .panneau-lateral__contenu {
padding: calc(var(--panneau-padding) / 2);
}
/* réintégration du grip */
.panneau-lateral--gauche.items-icons .panneau-lateral__contenu {
padding-right: calc((var(--panneau-padding) / 2) + var(--panneau-visible));
}
.panneau-lateral--droite.items-icons .panneau-lateral__contenu {
padding-left: calc((var(--panneau-padding) / 2) + var(--panneau-visible));
}
/* Augmentation de la taille des icones, et marge uniquement utile lorsqu'il y a le label, retirée */
.panneau-lateral.items-icons i {
margin-right: 0;
font-size: 3em;
}Le panneau ne se contente plus de s’ouvrir ou se fermer. Il devient capable d’adapter son contenu et son encombrement selon le contexte. Vous pouvez voir une version finalisée depuis Offcanvas 04 Triple zone d’affichage. Le tableau ci-dessous présente les nouvelles classes ajoutées à notre composant.
| Classe | Rôle | Effet |
|---|---|---|
items-icons |
Mode compact | Affiche uniquement les icônes, réduit la largeur et le padding |
items-labels |
Mode texte | Affiche uniquement les libellés, largeur adaptée |
items-both |
Mode complet | Affiche icônes et libellés, largeur maximale |
Un composant peut proposer plusieurs états visuels sans modifier sa mécanique d’implémentation.
Accessibilité : rendre le panneau utilisable par tous
Développer des fonctionnalités est une étape. Les rendre utilisables par tous en est une autre. Un panneau latéral, même simple, doit pouvoir être compris, activé et parcouru au clavier comme à la souris, et exposer des informations claires aux technologies d’assistance. Nous allons ici ajouter des attributs ARIA au composant, puis compléter le JavaScript avec des interactions clavier de base. Pour un rappel voir ARIA les bases : Rôles, repères, états et propriétés.
On enrichit le HTML sans changer la mécanique du composant. L’objectif est de décrire explicitement le rôle du panneau et son état courant, afin que ces informations ne reposent pas uniquement sur le rendu visuel. En ajoutant des attributs ARIA, on rend le composant compréhensible pour les technologies d’assistance et on aligne son comportement réel avec ce qui est perçu par tous les utilisateurs.
<div id="panneau-lateral-options"
class="panneau-lateral panneau-lateral--gauche items-both"
role="complementary"
aria-label="Panneau d’options"
aria-expanded="false"
aria-hidden="true"
tab-index="-1">
<!--
id => identifiant unique du composant
class => classe principale + position + mode d’affichage
role => zone complémentaire de la page (contenu secondaire)
aria-label => nom lisible par les technologies d’assistance
aria-expanded => état du panneau : fermé par défaut
aria-hidden => indique que le panneau est masqué
tab-index => permet de recevoir le focus par JavaScript
-->
<!-- conteneur interne du contenu -->
<div class="panneau-lateral__contenu">
<!-- élément interactif accessible au clavier -->
<div class="items" role="button" tabindex="0">
<!-- icône décorative ignorée par les lecteurs d’écran -->
<i class="bi bi-1-square" aria-hidden="true"></i>
<!-- libellé lisible -->
<span class="label">Item 1</span>
</div>
<!-- ... autres éléments identiques -->
</div>
</div> À chaque ouverture ou fermeture du panneau, il est important de synchroniser les attributs ARIA avec son état réel. Sans cela, une interface peut sembler fonctionner visuellement, tout en restant incohérente pour les technologies d’assistance. On va donc veiller à mettre à jour ces attributs à chaque interaction, afin que le composant reflète correctement son état ouvert ou fermé.
const panneau = document.getElementById('panneau-lateral-options');
function setEtat(open) {
panneau.setAttribute('aria-expanded', String(open));
panneau.setAttribute('aria-hidden', String(!open));
}
panneau.addEventListener('click', () => {
const open = panneau.classList.toggle('is-open');
setEtat(open);
});On ajoute des interactions simples, en s’appuyant sur des conventions largement utilisées dans les interfaces modernes. Ces raccourcis ne sont pas choisis au hasard : ils correspondent à des comportements attendus par les utilisateurs, notamment dans les menus, modales ou panneaux latéraux. En les reprenant ici, on rend le composant immédiatement plus compréhensible et plus naturel à utiliser, sans nécessiter d’apprentissage particulier.
EntréeouEspacepour ouvrir/fermerÉchappour fermerTabpour parcourir les items
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
const open = panneau.classList.toggle('is-open');
setEtat(open);
// donner explicitement le focus au panneau lorsqu’il s’ouvre
if (open) {
panneau.focus();
}
}
if (e.key === 'Escape') {
panneau.classList.remove('is-open');
setEtat(false);
}
});L’écoute est placée sur document. Autrement dit, le clavier est capté de manière globale, quel que soit l’élément actuellement ciblé dans la page. Dans le cadre de cet article, ce choix permet d’ouvrir et de fermer le panneau sans introduire de déclencheur HTML dédié comme un bouton ou un lien. La mise en place reste ainsi volontairement simple. Ce fonctionnement doit toutefois être bien compris. Il s’agit d’une solution fonctionnelle mais globale : les touches Entrée et Espace agissent ici sans tenir compte du contexte de focus. Concrètement, l’ouverture et la fermeture peuvent être déclenchées même si l’utilisateur n’interagit pas directement avec le panneau.
Dans une implémentation plus complète, on limiterait ces interactions à un élément précis, par exemple un véritable bouton de grip associé au panneau. Cela permet d’éviter des déclenchements involontaires ailleurs dans l’interface et de mieux cadrer l’expérience utilisateur. Ici, ce choix reste volontaire : il sert à mettre en évidence le mécanisme d’interaction sans alourdir la structure HTML. On est donc dans une étape d’illustration, avant d’introduire un déclencheur plus explicite dans une version plus aboutie du composant.
Concernant la navigation, le focus peut circuler entre les éléments grâce à tabindex, à condition que les éléments soient rendus focusables et que le panneau reçoive explicitement le focus lors de son ouverture. Cela crée un point d’entrée cohérent pour la navigation clavier. Pour aller plus loin, il serait possible de piéger le focus dans le panneau lorsqu’il est ouvert. La touche Tab resterait alors limitée aux éléments interactifs du panneau, au lieu de poursuivre dans le reste de la page. À la fermeture, le focus serait restitué à l’élément ayant déclenché l’ouverture. Ce comportement, plus avancé, dépasse le cadre de cette démonstration et pourra faire l’objet d’un approfondissement dédié.
Le panneau est désormais compréhensible, grâce à des rôles et des labels explicites, pilotable au clavier selon des conventions reconnues, et cohérent dans ses états via des attributs tels que
aria-expandedetaria-hidden. Autrement dit, une interface accessible ne demande pas de réécrire le composant, mais de rendre explicite ce qu’il fait et comment y interagir, pour que chaque utilisateur, quel que soit son contexte d’usage, puisse s’y retrouver naturellement.
Conclusion : un composant simple, des prolongements multiples
Au fil de cet article, nous avons construit un panneau latéral capable de s’ouvrir, se fermer, se positionner et adapter son contenu. Ce qui pouvait apparaître comme un simple effet d’interface s’est révélé être un composant structuré, reposant sur une mécanique claire, un habillage modulable et des comportements maîtrisés. Cette base volontairement simple n’est pas une fin en soi. Elle constitue un point d’appui pour aller plus loin, en fonction des besoins réels d’un projet.
- On peut par exemple envisager un chargement dynamique du contenu, en s’appuyant sur AJAX ou des appels API. Le panneau devient alors un espace capable d’afficher des informations contextuelles, mises à jour sans rechargement de page.
- La gestion du clavier peut également être approfondie. Nous avons abordé la navigation et les raccourcis essentiels, mais il est possible d’aller plus loin avec des mécanismes comme le focus trap, afin de contenir la navigation au sein du panneau lorsqu’il est actif, puis de restituer proprement le focus à sa fermeture.
- Autre évolution naturelle : l’introduction d’un véritable déclencheur HTML, souvent appelé grip. Contrairement au pseudo-élément utilisé ici, ce déclencheur devient un élément interactif à part entière, capable de s’adapter à différents états du panneau, de porter des attributs ARIA et de s’intégrer dans une logique d’interface plus complète.
Ces pistes montrent qu’un composant, même simple en apparence, peut évoluer progressivement vers des usages plus riches, sans remise en cause de sa structure initiale. L’essentiel reste de conserver cette logique de séparation : une mécanique stable, un habillage adaptable, et des comportements qui s’ajoutent sans complexifier inutilement l’ensemble.
Un bon composant n’est pas celui qui fait tout, mais celui qui peut évoluer sans se contredire.
