Parfois on a des besoins simples, parfois on pense qu'on va avoir besoin de sortir une masse de JavaScript alors que CSS fait déjà 99% du travail…
Ce qu'on va réaliser c'est ça :

L'idée est simple : on a un menu avec une liste d'item, on peut cliquer sur un des éléments, l'affichage change en dessous.
Pourquoi vouloir utiliser beaucoup plus de CSS plutôt que du JavaScript ? Déjà c'est déclaratif (vous définissez ce que vous voulez, pas comment vous voulez que ça fonctionne). Ensuite c'est beaucoup plus durable : le code CSS qui fonctionne aujourd'hui fonctionnera à coup sûr dans plusieurs années. Ensuite : c'est beaucoup, beaucoup plus performant !
Pour voir le menu en action : https://kuroidoruido.github.io/css-is-magic/radio-menu/index.html
Posons notre cadre de travail
On va commencer par quelque chose de simple, à savoir un template HTML basic :
<!DOCTYPE html>
<html>
<head>
<title>CSS is Magic</title>
<link rel="stylesheet" href="./reset.css" />
<link rel="stylesheet" href="./main.css" />
</head>
<body>
</body>
</html>
Pour l'instant côté main.css rien pour l'instant. Côté reset.css :
/* Pour s'assurer que la bordure des éléments ne fausse pas les dimensions */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Pour supprimer la marge sur body qui est généralement présente sur le style par défaut des différents navigateurs */
body {
margin: unset;
}
/* Pour s'assure d'utiliser la même police partout */
button,
input,
textarea,
select {
font: inherit;
}
/* Pour avoir un style plus simple par défaut sur les éléments visuels (essentiellement les images) */
img,
picture,
svg,
canvas {
display: block;
max-inline-size: 100%;
block-size: auto;
}
/* Pour couper les animations pour personnes que ça pourrait gêner */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Note : c'est le
reset.cssque j'utilise un peu partout, parce que je le trouve plus simple mais plutôt pratique. En vrai pour notre exemple, il n'y a que les deux premiers blocs qui sont nécessaires.
Posons notre menu !
Il existe surement d'autres façons de faire ça, personnellement je choisis une méthode que je trouve très simple : notre menu va être un formulaire qui contient des options radio. Pourquoi ? Parce qu'il existe un pseudo selector :checked qui permet de créer du style en fonction de si un élément radio est choisi !
Voici le contenu à mettre dans le body :
<header>
<a href="../index.html" title="Back to home">🏠</a>
<nav>
<label><input type="radio" name="bg" value="white" checked />White</label>
<label><input type="radio" name="bg" value="red" />Red</label>
<label><input type="radio" name="bg" value="green" />Green</label>
<label><input type="radio" name="bg" value="blue" />Blue</label>
<label><input type="radio" name="bg" value="black" />Black</label>
</nav>
</header>
<main>
<section class="white">
<h1>White</h1>
</section>
<section class="red">
<h1>Red</h1>
</section>
<section class="green">
<h1>Green</h1>
</section>
<section class="blue">
<h1>Blue</h1>
</section>
<section class="black">
<h1>Black</h1>
</section>
</main>
C'est la dernière fois qu'on touchera au HTML pour cet article, j'ai tout le contenu dont j'ai besoin pour faire un menu complet ! 🤓
L'idée va être d'afficher les sections une à une en fonction de quel bouton radio est sélectionné (si on choisit "White" on affiche la section "White", etc.). Notre menu est dans un bloc nav, qui est lui-même contenu dans un élément header qui contient aussi un lien vers la page d'accueil (ça c'est plutôt pour moi mais ça va me permettre de mettre en ligne plusieurs démos 100% CSS et permettre de naviguer entre les démos). Vous noterez que j'ai bien mis le même name à tous les radios (c'est comme ça que fonctionnent des radios), l'attribut checked sur le premier permet d'avoir une sélection par défaut.
Et le style de base qu'on va utiliser :
body {
& > main {
height: 90vh;
& > section {
display: flex;
border: 2px dashed currentColor;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
color: black;
}
section.white {
background-color: white;
}
section.red {
background-color: crimson;
}
section.green {
background-color: lightgreen;
}
section.blue {
background-color: aqua;
}
section.black {
color: white;
background-color: black;
}
}
}
Note : J'utilise des syntaxes modernes (nesting ici, plus tard
:has()) de CSS qui sont supportés par les navigateurs modernes, et je fais mes tests sur Firefox 139 (mai 2025).
J'ai juste mis un peu de style basique pour les sections qu'on voit quelque chose d'un peu propre.
Avec ça, vous aurez quelque chose du genre :

Comment agir au changement ?
Pour ça c'est très simple, on va combiner la pseudo-classe :has() (qui permet de sélectionner un élément en fonction de son contenu) et le pseudo-sélecteur :checked pour savoir quel radio est sélectionné !
Par exemple body:has(input[name="bg"][value="white"]:checked) section.white signifie que je veux partir du body qui a un input avec un attribut name qui vaut bg (pour être certain de cibler le bon radio) et dont la valeur est white et qui est sélectionné actuellement (= :checked), si j'ai un tel body, alors je prends la section blanche (= section.white).
Si on décline pour tous les radios :
body {
& > main {
& > section {
display: none; /* à remplacer dans le style que j'avais donné plus haut */
}
}
&:has(header nav input[name="bg"][value="white"]:checked) section.white {
display: flex;
}
&:has(header nav input[name="bg"][value="red"]:checked) section.red {
display: flex;
}
&:has(header nav input[name="bg"][value="green"]:checked) section.green {
display: flex;
}
&:has(header nav input[name="bg"][value="blue"]:checked) section.blue {
display: flex;
}
&:has(header nav input[name="bg"][value="black"]:checked) section.black {
display: flex;
}
}
Ici on a donc par défaut toutes les sections qui sont masquées, et on les affiche dès qu'on a le bon radio qui est sélectionné.

Ça fonctionne ! 🤓 Mais c'est moche… 😰 On va corriger ça !
Un peu de look pour notre menu !
Pour commencer : faisons en sorte que notre menu prenne toute la largeur de l'écran, et aligner tout comme il faut !
body {
& > header {
display: flex;
a {
flex: 0 1 0;
}
nav {
flex: 1 1;
display: flex;
justify-content: center;
}
}
}

Ajustons la couleur, l'espacement, la taille du texte pour le lien retour.
body {
& > header {
display: flex;
align-items: center;
padding: 0.5rem;
background-color: black;
color: white;
a {
flex: 0 1 0;
font-size: 2rem;
line-height: 2rem;
}
nav {
flex: 1 1;
display: flex;
justify-content: center;
& > label {
padding: 0.5rem 1rem;
}
}
}
}

Avec une mise en avant de l'élément sélectionné toujours via :checked.
body {
& > header {
nav {
& > label {
border-radius: 1rem;
&:has(input:checked) {
background-color: white;
color: black;
}
}
}
}
}

Et maintenant, il faut faire disparaitre visuellement le radio, ne garder que le label, mais sans utiliser display: none ou sortir le radio de l'écran, sinon notre menu ne serait plus accessible (et l'accessibilité c'est important !). On va utiliser un combo position: absolute (qui va placer le radio au centre du label, mais toujours affiché) et opacity: 0 de sorte à avoir une transparence complète !
body {
& > header {
nav {
& > label {
input[type="radio"] {
position: absolute;
opacity: 0;
}
}
}
}
}

On va ajouter les derniers détails : mettre en avant le focus (si vous faite tab sur les radios, vous ne vous le voyez plus) et le survole.
body {
& > header {
nav {
& > label {
border: 0.25rem solid black;
cursor: pointer;
&:has(input:focus),
&:hover {
border-color: gray;
}
}
}
}
}

Conclusion
Ce n'est pas le plus beau menu car je n'ai pas poussé très loin le style, mais le comportement qu'on peut attendre d'un tel menu est bien présent ! J'ai préféré rester plutôt simple / sobre / minimaliste pour rester concentrer sur le plus important côté style, et mettre en avant les points qu'il faut prendre en compte !
Comme promis : ce menu fonctionne 100% sans JavaScript, est complètement natif sur les navigateurs récents (sauf erreur des 3 dernières années), donc c'est quelque chose qu'on peut faire sans trop se prendre la tête côté compatibilité !
N'hésitez pas à me dire si vous utilisez quelque chose du genre, je suis toujours curieux de voir ce qu'on peut faire avec uniquement du CSS !
Crédit photo : Générée via Mistral AI avec le prompt suivant :
A detailed illustration in the style of Studio Ghibli: A fox character dressed as a wizard, standing in a magical workshop filled with floating CSS code snippets and glowing HTML tags. The fox is holding a wand shaped like a CSS curly brace { }, casting a spell that brings to life a colorful, interactive menu made of radio buttons and colored sections (white, red, green, blue, black). The menu elements float around the fox, glowing softly with a magical aura. The background features shelves filled with potion bottles labeled with CSS properties like ':checked', ':has()', and 'display: flex'. The scene is warm and inviting, with soft lighting and a cozy atmosphere, emphasizing the magic of CSS. The fox has a focused, determined expression, showcasing the power and creativity of front-end development without JavaScript.