642 tutoriels en ligne

Créa-blog

#100JoursPourCoder
Projet Créa-code | Formation Créa-troyes

Ressources pour développeur web

Théme de la semaine : Search Console

Onglet ou Tabs en CSS pure avec Grid et Subgrid

Temps de lecture estimé : 10 minutes
Accueil HTML5 Onglet ou Tabs en CSS pure avec Grid et Subgrid

Les onglets ou tabs sont partout sur le web… mais souvent, ils reposent sur du JavaScript inutile. Et si vous pouviez créer un système de navigation par onglet 100 % CSS, propre, moderne et performant ? Grâce à Grid et à Subgrid, il est désormais possible de structurer des interfaces élégantes, dynamiques et parfaitement alignées, sans une seule ligne de JS.

  • Une méthode moderne pour concevoir des interfaces à onglets ou tags sans JavaScript, en exploitant uniquement les éléments HTML <details> et <summary> combinés à CSS Grid et Subgrid, pour une UI plus légère et robuste.
  • Un système d’onglets qui s’appuie sur des standards natifs pour améliorer l’accessibilité et la cohérence des interactions dans les navigateurs.
  • Une approche qui facilite la maintenance et l’alignement des contenus grâce à Grid et Subgrid, tout en offrant une structure plus claire.

Dans ce tutoriel, nous allons voir comment exploiter la puissance du CSS moderne pour concevoir des tabs flexibles, accessibles et réellement professionnels.

On commence par le HTML

Posons des bases propres. Avant même de parler de CSS, il faut une structure HTML claire et logique. Ici, nous allons utiliser plusieurs éléments <details> regroupés à l’intérieur d’un conteneur parent que nous appellerons .grid.

Pourquoi ce choix ? Parce que chaque bloc <details> va représenter un onglet de notre interface. Autrement dit, chaque élément devient un .item, exactement comme si vous manipuliez des “tabs” classiques… sauf qu’ici, on s’appuie sur du HTML natif et sémantique.

Le point fort de cette approche, c’est qu’on ne bricole rien : on exploite des éléments prévus pour afficher/masquer du contenu. Résultat ? Une base propre, compréhensible, et surtout parfaitement compatible avec notre futur layout en Grid et Subgrid.

<div class="grid">
  <!-- Premier onglet -->
  <details class="item" name="alpha" open>
    <summary class="subitem">Premier item</summary>
    <div><!-- Contenu --></div>
  </details>
  <details class="item" name="alpha">
    <summary class="subitem">Second item</summary>
    <div><!-- Contenu --></div>
  </details>
  <details class="item" name="alpha">
    <summary class="subitem">Troisième item</summary>
    <div><!-- Contenu --></div>
  </details>
</div>
Premier item
Second item
Troisième item

Pour l’instant, cela ne ressemble pas encore à de vrais onglets. Et c’est normal.

Ce que nous cherchons ici, ce n’est pas le rendu visuel, mais la bonne structure. Une base propre, cohérente et prête à être stylisée.

C’est seulement ensuite que le CSS va entrer en scène. Et là, nous allons vraiment exploiter la puissance de Grid et surtout de Subgrid pour transformer cette structure brute en véritables tabs modernes, alignés et élégants.

👉 Pour aller plus loin : Mise en page avec Grid et Grilles imbriquées avec subgrid

Puis on passe au CSS

Maintenant que notre HTML est propre, on peut passer aux choses sérieuses. Et là, surprise… on utilise CSS Grid. Oui, évidemment.

L’idée est simple : transformer notre wrapper .grid en une grille à trois colonnes. Une colonne par onglet, autrement dit une colonne par .item, avec un léger espace entre chacune pour éviter l’effet “bloc collé”.

Mais ce n’est pas tout, on définit aussi deux lignes dans cette grille. La première s’adapte naturellement à la hauteur du contenu — elle accueillera nos onglets. La seconde, elle, va occuper l’espace restant disponible. Et c’est précisément là que s’affichera le contenu de l’onglet actif.

En clair, on sépare visuellement la navigation (les tabs) du panneau de contenu… tout ça sans JavaScript, uniquement grâce à la logique puissante de Grid. Et ce n’est que le début.

.grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(200px, 1fr));
  grid-template-rows: auto 1fr;
  column-gap: 1rem;
}

Cela commence de plus en plus à ressembles à des onglets :

Premier item
Second item
Troisième item

Mise en place de subgrid pour nos onglets

Pourquoi Subgrid ? Parce qu’il nous permet de réutiliser directement les lignes de la grille principale .grid, sans devoir recréer une nouvelle grille imbriquée avec ses propres colonnes et ses propres repères. Résultat : tout reste parfaitement aligné, propre et cohérent. Pas de décalage bizarre, pas de calculs approximatifs.

Concrètement, chaque onglet — donc chaque élément <details> — devient lui-même une grille. Mais au lieu de définir de nouvelles colonnes et lignes, on indique simplement qu’elles doivent hériter de celles de la .grid grâce à subgrid.

C’est là que ça devient intéressant !

On garde une structure logique, un alignement millimétré entre les onglets et leur panneau de contenu, tout en évitant la complexité des grilles imbriquées classiques. Moins de code, moins de friction, plus de maîtrise. Bienvenue dans le CSS moderne.

details {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
}

En plus de cela, on veut que chaque onglet occupe toute la surface de la .grid. L’objectif est clair : éviter les petits blocs flottants et faire en sorte que chaque <details> s’inscrive pleinement dans la structure globale.

Pour y parvenir, on utilise les propriétés grid-column et grid-row. Elles permettent d’indiquer que l’élément <details> doit s’étendre sur toute la largeur disponible, mais aussi sur toute la hauteur définie par la grille.

Autrement dit, chaque onglet ne se contente pas d’être posé dans une case : il occupe entièrement l’espace prévu par la grille principale. Cette approche garantit un alignement parfait entre la navigation et le panneau de contenu, tout en conservant une structure claire et maîtrisée.

On ne laisse rien au hasard. Chaque élément connaît précisément sa place.

details {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
  grid-column: 1 / -1;
  grid-row: 1 / span 3;
}
Premier item
Second item
Troisième item

Ensuite, on va positionner le contenu du panneau d’onglet sur la deuxième ligne du subgrid, et l’étendre sur les trois colonnes. Ainsi, peu importe l’onglet actif, son contenu occupe toute la largeur disponible sous la barre des tabs.

Pour cibler ce contenu, on utilise le pseudo-élément ::details-content. Il bénéficie déjà d’un bon support navigateur (même si, en Février 2026, WebKit n’est pas encore totalement compatible).

Grâce à ::details-content, on peut styliser directement la zone interne du <details> sans ajouter un wrapper supplémentaire dans le HTML juste pour le design. On garde donc un balisage minimal, propre et sémantique… tout en contrôlant précisément l’affichage.

Moins de div inutiles, plus de maîtrise CSS.

details::details-content {
  grid-row: 2; /* position sur la seconde ligne */
  grid-column: 1 / -1;
  padding: 1rem;
  border-bottom: 2px solid dodgerblue;
}

Dans une interface à onglets, il y a une règle essentielle : un seul panneau doit être visible à la fois. Pas trois ouverts, pas deux à moitié affichés. Un seul actif, clairement identifié.

Heureusement, l’élément <details> nous simplifie la vie. Lorsqu’il est ouvert, il possède automatiquement l’attribut [open]. On peut donc cibler cet état directement en CSS.

L’astuce consiste à sélectionner les <details> qui ne sont pas ouverts avec :not([open]), puis à masquer leur ::details-content. Autrement dit, on laisse visible uniquement le panneau actif, et on cache les autres proprement.

Pas de JavaScript. Pas de logique complexe. Juste des sélecteurs CSS bien utilisés. C’est exactement ce qui rend cette approche aussi puissante qu’élégante.

details:not([open])::details-content {
  display: none;
}

Nous avons encore des onglets qui se chevauchent, mais le seul panneau d’onglets que nous affichons est actuellement ouvert, ce qui nettoie un peu les choses :

Premier item
Contenu de l’onglet numéro 1
Second item
Contenu de l’onglet numéro 2
Troisième item
Contenu de l’onglet numéro 3

Transformer <details> en vrais onglets ou Tabs

On arrive enfin à la partie intéressante.

Pour le moment, nos onglets sont toujours superposés visuellement. Structurellement, tout est en place… mais côté interface, ce n’est pas encore ça. L’objectif maintenant est simple : les répartir proprement sur la première ligne de la .grid, comme de vrais tabs alignés.

Chaque élément <details> contient un <summary>. Et ce <summary> joue un double rôle : il affiche le label de l’onglet et sert de bouton pour ouvrir ou fermer son contenu. Autrement dit, navigation et interaction sont déjà intégrées nativement.

On va donc positionner le <summary> sur la première ligne du subgrid, celle dédiée à la barre d’onglets. Ensuite, on applique un léger style spécifique lorsque le <details> est dans l’état [open]. Cela permet de différencier visuellement l’onglet actif des autres.

À ce stade, on ne code pas une fausse navigation. On transforme intelligemment une structure HTML native en une interface d’onglets moderne, propre et totalement contrôlée en CSS.

summary {
  grid-row: 1; 
  display: grid;
  padding: 1rem; 
  border-bottom: 2px solid dodgerblue;
  cursor: pointer; 
}

/* L'élément <summary> quand <details> est [open] */
details[open] summary {
  font-weight: bold;
}

On y est presque.

Formation web et informatique - Alban Guillier - Formateur

Des formations informatique pour tous !

Débutant ou curieux ? Apprenez le développement web, le référencement, le webmarketing, la bureautique, à maîtriser vos appareils Apple et bien plus encore…

Formateur indépendant, professionnel du web depuis 2006, je vous accompagne pas à pas et en cours particulier, que vous soyez débutant ou que vous souhaitiez progresser. En visio, à votre rythme, et toujours avec pédagogie.

Découvrez mes formations Qui suis-je ?

Il reste une dernière étape : positionner correctement les <summary> dans les colonnes du subgrid pour qu’ils arrêtent de se chevaucher. Pour l’instant, ils occupent le même espace, ce qui explique qu’ils se bloquent visuellement les uns les autres.

La solution ? Utiliser le pseudo-sélecteur :nth-of-type pour cibler chaque élément individuellement selon son ordre dans le HTML. Ainsi, on peut attribuer à chaque <summary> sa propre colonne dans la grille.

Maintenant, chaque onglet prend naturellement sa place, parfaitement aligné, sans conflit ni superposition. Une fois cette étape réalisée, l’interface ressemble enfin à de vrais tabs… mais toujours avec zéro JavaScript.

/* Premier item de la première colonne */
details:nth-of-type(1) summary {
  grid-column: 1 / span 1;
}
/* Second item de la seconde colonne */
details:nth-of-type(2) summary {
  grid-column: 2 / span 1;
}
/* Troisième item de la troisième colonne */
details:nth-of-type(3) summary {
  grid-column: 3 / span 1;
}

Nos onglets sont uniformément répartis le long de la rangée supérieure de la sous-grille :

Premier item
Contenu de l’onglet numéro 1
Second item
Contenu de l’onglet numéro 2
Troisième item
Contenu de l’onglet numéro 3

Petit bémol : en CSS, on ne peut pas utiliser de boucles comme dans un langage de programmation classique. Pas encore en tout cas.

Mais cela ne veut pas dire qu’on doit répéter du code inutilement.

On peut s’appuyer sur les variables CSS (custom properties) pour garder des styles propres, factorisés et faciles à maintenir. Plutôt que de dupliquer des valeurs partout, on centralise ce qui doit être partagé.

Moins de répétition, plus de lisibilité. Bref, on garde un CSS vraiment… léger :

summary {
  grid-column: var(--n) / span 1;
}

Maintenant, il faut définir la variable --n pour chaque élément <details>.

ici, nous allons déclarer ces variables directement dans le HTML. Cela permet de les utiliser comme de véritables points d’ancrage pour le styling.

En pratique, chaque onglet reçoit sa propre valeur --n, ce qui nous donne un moyen simple et propre de contrôler son positionnement ou son comportement en CSS. Pas besoin de complexifier la feuille de style : on transmet l’information depuis le balisage, puis on l’exploite intelligemment côté CSS.

C’est une approche très pragmatique : le HTML structure et fournit le contexte, le CSS interprète et met en forme.

<div class="grid">
  <details class="item" name="alpha" open style="--n: 1">
    <summary class="subitem">Premier item</summary>
    <div>Contenu de l'onglet numéro 1</div>
  </details>
  <details class="item" name="alpha" style="--n: 2">
    <summary class="subitem">Second item</summary>
    <div>Contenu de l'onglet numéro 2</div>
  </details>
  <details class="item" name="alpha" style="--n: 3">
    <summary class="subitem">Troisème item</summary>
    <div>Contenu de l'onglet numéro 3</div>
  </details>
</div>

Encore une fois, comme le CSS ne propose pas (encore) de boucles natives, on va déléguer cette logique à un langage de template. Liquid fait très bien le travail.

L’idée est simple : au lieu d’écrire manuellement le HTML de chaque onglet, on génère la structure via une boucle côté template. On gagne du temps, on évite les répétitions, et surtout on réduit le risque d’erreur si le nombre d’onglets change.

Le CSS reste concentré sur le style. Le moteur de template, lui, s’occupe de produire un HTML propre et dynamique. Chacun son rôle, et le code reste lisible.

{% for item in itemList %}
  <div class="grid">
    <details class="item" name="alpha" style="--n: {{ forloop.index }}" {% if forloop.first %}open{% endif %}>
      <!-- etc. -->
    </details>
  </div>
{% endfor %}

Vous pouvez bien sûr utiliser un autre langage de template. Il en existe beaucoup, et chacun a ses préférences. L’important, c’est de garder une génération propre et concise du HTML, surtout quand la structure devient répétitive.

La touche finale

Il reste encore un petit détail à régler.

Actuellement, un seul <summary> est réellement cliquable : le dernier. Pourquoi ? Parce que tous les blocs <details> sont empilés les uns sur les autres, et celui qui arrive en dernier dans le DOM se retrouve tout en haut de la pile.

Résultat : il masque les autres et capte tous les clics.

Ce n’est pas un bug mystérieux. C’est simplement le fonctionnement normal du flux et du stacking context en CSS. Il faut donc ajuster cela pour que chaque onglet puisse être interactif, sans être bloqué par celui du dessus.

Vous l’avez peut-être déjà deviné : il faut simplement placer nos <summary> au-dessus des autres éléments.

  • Pour cela, on utilise z-index.

En ajustant correctement le z-index, on s’assure que chaque <summary> passe au premier plan dans la pile d’affichage. Ainsi, ils ne sont plus masqués par les autres blocs <details> superposés, et chacun redevient parfaitement cliquable.

Un petit réglage, mais essentiel pour que toute l’interface fonctionne comme prévu.

summary {
  z-index: 1;
}

Voici la démonstration complète de notre travail :

Premier item
Contenu de l’onglet numéro 1
Second item
Contenu de l’onglet numéro 2
Troisième item
Contenu de l’onglet numéro 3

Accessibilité

L’un des gros avantages de l’élément <details>, c’est qu’il embarque déjà des fonctionnalités d’accessibilité natives. Navigation au clavier, gestion correcte des états ouvert/fermé, compatibilité avec les lecteurs d’écran… tout cela fonctionne sans effort supplémentaire.

Autrement dit, on ne simule pas un comportement interactif : on s’appuie sur un composant HTML pensé pour ça.

Bien sûr, on pourrait aller encore plus loin et affiner certains aspects pour renforcer l’expérience utilisateur. Mais cela mériterait presque un article à part entière.

Et comme toujours en développement web, le sujet gagne à être enrichi : tester, challenger, améliorer.

Le code entier de notre navigation par onglet ou Tabs en CSS pure

<div class="grid">
  <!-- Premier onglet -->
  <details class="item" name="alpha" open style="--n: 1">
    <summary class="subitem">Premier item</summary>
    <div>Contenu de l'onglet numéro 1</div>
  </details>
  <!-- Deuxième onglet -->
  <details class="item" name="alpha" style="--n: 2">
    <summary class="subitem">Second item</summary>
    <div>Contenu de l'onglet numéro 2</div>
  </details>
  <!-- Troisième onglet -->
  <details class="item" name="alpha" style="--n: 3">
    <summary class="subitem">Troisième item</summary>
    <div>Contenu de l'onglet numéro 3</div>
  </details>
</div>
<style>
.grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(200px, 1fr));
  grid-template-rows: auto 1fr;
  column-gap: 1rem;
}
.grid details {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
  grid-column: 1 / -1;
  grid-row: 1 / span 3;
}
.grid details::details-content {
  grid-row: 2; 
  grid-column: 1 / -1; 
  padding: 1rem;
  border-bottom: 2px solid dodgerblue;
}
.grid details:not([open])::details-content {
  display: none;
}
.grid summary {
  grid-row: 1; 
  display: grid;
  grid-column: var(--n) / span 1;
  padding: 1rem; 
  border-bottom: 2px solid dodgerblue;
  cursor: pointer; 
  z-index: 1;
}

.grid details[open] summary {
  font-weight: bold;
}

/* Premier item de la première colonne */
.grid details:nth-of-type(1) summary {
  grid-column: 1 / span 1;
}
/* Deuxième item de la seconde colonne */
.grid details:nth-of-type(2) summary {
  grid-column: 2 / span 1;
}
/* Troisième item de la troisième colonne */
.grid details:nth-of-type(3) summary {
  grid-column: 3 / span 1;
}
</style>

Une navigation par onglet au design amélioré avec animation

En gardant notre structure HTML, voici une optimisation CSS

<style>
/* =========================
   Wrapper principal
========================= */
.grid6 {
  display: grid6;
  grid6-template-columns: repeat(3, minmax(0, 1fr));
  grid6-template-rows: auto 1fr;
  column-gap: 0;
  max-width: 800px;
  margin: 3rem auto;
  border-radius: 12px;
  overflow: hidden;
  border: 1px solid #e5e7eb;
  background: #ffffff;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
  font-family: system-ui, -apple-system, sans-serif;
}

/* =========================
   Subgrid6 setup
========================= */
.grid6 details {
  display: grid6;
  grid6-template-columns: subgrid6;
  grid6-template-rows: subgrid6;
  grid6-column: 1 / -1;
  grid6-row: 1 / span 2;
}

/* =========================
   Contenu panneau
========================= */
.grid6 details::details-content {
  grid6-row: 2;
  grid6-column: 1 / -1;
  padding: 2rem;
  background: #fafafa;
  border-top: 1px solid #e5e7eb;
  animation: fadeIn 0.25s ease;
}

.grid6 details:not([open])::details-content {
  display: none;
}

.panel h3 {
  margin-top: 0;
  margin-bottom: 1rem;
  font-size: 1.2rem;
}

.panel p {
  margin-bottom: 1rem;
  line-height: 1.6;
  color: #374151;
}

.panel ul {
  padding-left: 1.2rem;
  margin: 0;
  line-height: 1.6;
}

/* =========================
   Onglets (summary)
========================= */
.grid6 summary {
  list-style: none;
  grid6-row: 1;
  grid6-column: var(--n);
  padding: 1rem 1.2rem;
  text-align: center;
  cursor: pointer;
  font-weight: 500;
  background: #f9fafb;
  border-bottom: 1px solid #e5e7eb;
  transition: background 0.2s ease, color 0.2s ease;
  z-index: 2;
}

.grid6 summary::-webkit-details-marker {
  display: none;
}

/* Onglet actif */
.grid6 details[open] summary {
  background: #ffffff;
  border-bottom: 2px solid #2563eb;
  color: #2563eb;
  font-weight: 600;
}

/* Hover */
.grid6 summary:hover {
  background: #f3f4f6;
}

/* =========================
   Animation
========================= */
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}
</style>
Présentation

Des onglets 100% CSS

Cette démonstration utilise uniquement HTML natif, CSS grid6 et Subgrid6. Aucun JavaScript n’est nécessaire pour gérer l’ouverture et la fermeture des panneaux.

Résultat : un composant plus léger, plus accessible et plus facile à maintenir dans le temps.

Fonctionnement

Une structure intelligente

Chaque onglet repose sur l’élément <details>, qui gère nativement l’état ouvert/fermé.

Grâce à Subgrid6, tous les éléments restent parfaitement alignés sans recréer de grille imbriquée complexe.

Avantages

Pourquoi adopter cette méthode ?

  • Moins de JavaScript
  • Meilleure accessibilité native
  • Alignement précis grâce à Subgrid6
  • Maintenance simplifiée

Créer des onglets en CSS pur avec <details>, Grid et Subgrid, ce n’est pas seulement une prouesse technique moderne. C’est surtout une manière plus propre, plus performante et plus accessible de concevoir une interface à tabs sans dépendre du JavaScript. En combinant structure sémantique, alignement précis grâce à Subgrid et gestion native des états ouverts, vous obtenez un composant léger, maintenable et parfaitement adapté aux standards actuels du web.

Si vous cherchez à maîtriser le CSS moderne et à construire des interfaces robustes, ces tabs en CSS pur sont un excellent terrain d’entraînement.