Le Toggle Switch

Forgez un Toggle button 100% natif, ultra-léger et accessible en utilisant uniquement une checkbox HTML et la puissance du CSS moderne.

Auteur :
Jonathan Marco
Difficulté :
Initiation au rêve (débutant)

Forgez un Toggle button 100% natif, ultra-léger et accessible en utilisant uniquement une checkbox HTML et la puissance du CSS moderne.

Notre Arsenal se dote d'un interrupteur natif et minimaliste ! Aujourd'hui, on met la main sur une pièce de stuff indispensable pour vos interfaces : le Toggle Switch (ou interrupteur). Très souvent, pour obtenir ce rendu, les frameworks nous fournissent une imbrication infernale de <div> illisibles.

Pourtant, avec les évolutions récentes de CSS, on peut forger un interrupteur fonctionnel, fluide et accessible avec un simple <input> !

Le loot maudit (L'approche Framework)

Si on inspecte un composant "Toggle" généré par un framework UI classique (ici celui de Quasar), voici le genre d'équipement corrompu sur lequel on tombe :

<div class="q-toggle cursor-pointer no-outline row inline no-wrap items-center q-toggle--dark" tabindex="0" role="switch" aria-label="On Right" aria-checked="true">
  <div class="q-toggle__inner relative-position non-selectable q-toggle__inner--truthy" aria-hidden="true">
    <input class="hidden q-toggle__native absolute q-ma-none q-pa-none" type="checkbox"/>
    <div class="q-toggle__track"></div>
    <div class="q-toggle__thumb absolute flex flex-center no-wrap"></div>
  </div>
  <span class="no-outline" tabindex="-1"></span>
  <div class="q-toggle__label q-anchor--skip">On Right</div>
</div>

Bilan : 7 balises, des classes CSS à rallonge, un faux bouton qui simule un comportement natif via JavaScript, un code illisible... C'est un item lourd, fragile, et qui va plomber vos performances.

On peut trouver bien meilleur équipement. Et surtout, beaucoup plus simple.

Le stuff de qualité (HTML Natif)

La base de notre composant repose sur l'élément HTML le plus logique pour gérer un état binaire (Vrai/Faux, On/Off) : la checkbox.

<input type="checkbox" id="theme-toggle" role="switch" class="toggle">
<label for="theme-toggle" class="sr-only">Activer le mode sombre</label>

La magie CSS : Forger l'interrupteur

C'est ici que la forge s'active. Au lieu de cacher la checkbox pour recréer un faux bouton à côté, on va utiliser la propriété appearance: none; pour réinitialiser le style natif du navigateur, et on va dessiner directement sur l'input.

Voici le plan de craft complet (utilisant l'imbrication CSS native) :

.toggle {
    /* On définit la hauteur de notre élément */
    --toggle-height: 1.5rem;


    /* On désactive le style par défaut du système d'exploitation */
    appearance: none;


    /* On définit les dimensions de la "piste" (le fond) */
    width: 3rem;
    height: var(--toggle-height);
    border-radius: 99px; /* Un grand border-radius pour l'effet pilule */
    background: #334155; /* Couleur quand c'est inactif (gris bleuté) */
    position: relative;
    cursor: pointer;
    transition: 0.3s;


    /* On crée la "pastille" (le bouton qui glisse) avec un pseudo-élément
    * Le pseudo-élément est intéressant car le rond qui indique l'état n'a pas de sens sémantique,
    * ajouter un élément juste graphique est de la responsabilité de CSS.
    */
    &::after {
        /* On définit la hauteur et le placement de notre pastille */
        --element-top-left: 2px;
        --element-size: calc(var(--toggle-height) - var(--element-top-left) * 2);


        content: '';
        position: absolute;
        left: var(--element-top-left);
        top: var(--element-top-left);
        width: var(--element-size);
        height: var(--element-size);
        background: white;
        border-radius: 50%;
        transition: 0.3s;
    }


    /* On gère l'état "Activé" grâce à la pseudo-classe :checked natif aux checkbox */
    &:checked {
        background: #22c55e; /* Couleur quand c'est actif (vert néon) */


        /* On déplace la pastille vers la droite */
        &::after {
            transform: translateX(var(--toggle-height));
        }
    }
}

Comment ça marche ?

  • Le Reset (appearance: none;) : C'est le cheat code de cette pièce d'équipement. Il dit au navigateur : "Oublie comment tu dessines une checkbox d'habitude, c'est moi qui prends le relais". L'élément redevient une simple boîte qu'on peut styliser à volonté, tout en gardant sa logique interne, donc son comportement interactif et son état : coché/non coché.
  • Le pseudo-élément (::after) : C'est notre petite pastille blanche. En le positionnant en absolu à l'intérieur de notre input (position: relative;), on peut le placer parfaitement à gauche.
  • L'interaction (:checked) : Dès que l'utilisateur clique (ou appuie sur "Espace" au clavier), l'input passe à l'état :checked. Notre CSS détecte ce changement d'état, change la couleur de fond, et applique un transform: translateX() sur la pastille pour la faire glisser à droite.

Bonus: Enchanter le curseur (Icônes SVG)

Pour pousser le style de notre stuff au niveau légendaire, on peut ajouter un indicateur visuel (soleil/lune) directement à l'intérieur de notre pastille (le ::after).

L'astuce consiste à utiliser nos SVG en tant que contenu de notre pseudo-élément ::after avec l'encodage data:image/svg+xml !

.toggle {
    /* On définit la hauteur de notre élément */
    --toggle-height: 1.5rem;


    /* On désactive le style par défaut du système d'exploitation */
    appearance: none;


    /* On définit les dimensions de la "piste" (le fond) */
    width: 3rem;
    height: var(--toggle-height);
    border-radius: 99px; /* Un grand border-radius pour l'effet pilule */
    background: #334155; /* Couleur quand c'est inactif (gris bleuté) */
    position: relative;
    cursor: pointer;
    transition: 0.3s;


    /* On crée la "pastille" (le bouton qui glisse) avec un pseudo-élément
    * Le pseudo-élément est intéressant car le rond qui indique l'état n'a pas de sens sémantique,
    * ajouter un élément juste graphique est de la responsabilité de CSS.
    */
    &::after {
        /* On définit la hauteur et le placement de notre pastille */
        --element-top-left: 2px;
        --element-size: calc(var(--toggle-height) - var(--element-top-left) * 2);


        content: '';
        content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f59e0b'%3E%3Cpath d='M3.55 19.09L4.96 20.5L6.76 18.71L5.34 17.29M12 6C8.69 6 6 8.69 6 12S8.69 18 12 18 18 15.31 18 12C18 8.68 15.31 6 12 6M20 13H23V11H20M17.24 18.71L19.04 20.5L20.45 19.09L18.66 17.29M20.45 5L19.04 3.6L17.24 5.39L18.66 6.81M13 1H11V4H13M6.76 5.39L4.96 3.6L3.55 5L5.34 6.81L6.76 5.39M1 13H4V11H1M13 20H11V23H13'/%3E%3C/svg%3E");
        padding: 3px;
        box-sizing: border-box;
        position: absolute;
        left: var(--element-top-left);
        top: var(--element-top-left);
        width: var(--element-size);
        height: var(--element-size);
        background: white;
        border-radius: 50%;
        transition: 0.3s;
    }


    /* On gère l'état "Activé" grâce à la pseudo-classe :checked natif aux checkbox */
    &:checked {
        background: #22c55e; /* Couleur quand c'est actif (vert néon) */


        /* On déplace la pastille vers la droite */
        &::after {
            content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%231e293b'%3E%3Cpath d='M17.75,4.09L15.22,6.03L16.13,9.09L13.5,7.28L10.87,9.09L11.78,6.03L9.25,4.09L12.44,4L13.5,1L14.56,4L17.75,4.09M21.25,11L19.61,12.25L20.2,14.23L18.5,13.06L16.8,14.23L17.39,12.25L15.75,11L17.81,10.95L18.5,9L19.19,10.95L21.25,11M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.64 6.35,17.66C9.37,20.67 14.19,20.78 17.33,17.97Z'/%3E%3C/svg%3E");
            transform: translateX(var(--toggle-height));
        }
    }
}

Équipement validé

Avec ce composant natif ajouté à votre inventaire, vous gagnez sur tous les tableaux :

  • Zéro JavaScript : L'état On/Off est géré par le HTML, l'animation par le CSS.
  • 100% Accessible : La navigation au clavier (Tabulation + Espace) fonctionne nativement.
  • Ultra léger : Une seule balise (si on exclut le label) contre une usine à gaz de div imbriquées.

Vous avez maintenant de quoi équiper vos applications web avec des réglages d'une élégance absolue pour vos prochaines runs.

  • Cours
  • Tutoriels
  • Composants
  • Aide/FAQ
  • À Propos
  • Me connecter