623 tutoriels en ligne

Créa-blog

#100JoursPourCoder
Projet Créa-code

Ressources pour développeur web

Théme de la semaine : Search Console

Coder une combobox en CSS et JS : Guide + code complet

Temps de lecture estimé : 15 minutes
Accueil HTML5 Coder une combobox en CSS et JS : Guide + code complet

Vous avez sûrement déjà rempli un formulaire où il fallait choisir un pays, une ville ou un produit dans une liste interminable. Vous commencez à faire défiler, vous perdez patience, vous ratez l’option voulue… C’est précisément ce genre de situation que la combobox vient résoudre.

  • Comprendre quand et pourquoi utiliser une combobox pour améliorer l’expérience utilisateur de vos formulaires.
  • Acquérir une vision claire et structurée de l’interaction entre HTML, CSS et JavaScript à travers un composant concret et réutilisable.
  • Gagner en autonomie pour concevoir des interfaces modernes, fiables et professionnelles.

Dans ce tutoriel, nous allons apprendre à coder une combobox en CSS et JavaScript, pas à pas, en partant de zéro. L’objectif n’est pas seulement de copier un code qui fonctionne, mais de comprendre chaque brique, chaque choix technique et chaque ligne importante. À la fin, vous saurez exactement ce qu’est une combobox, quand l’utiliser, et comment en créer une, même si vous débutez en développement web ou FrontEnd.

Qu’est-ce qu’une combobox ?

Une combobox est un composant d’interface qui combine deux éléments très courants du web :

  • Un champ de saisie dans lequel l’utilisateur peut taper du texte.
  • Une liste de suggestions qui s’affiche dynamiquement en fonction de ce qui est saisi.

Contrairement à un simple champ <select> HTML, une combobox permet de rechercher dans les options. L’utilisateur n’est plus obligé de parcourir visuellement toute la liste. Il tape quelques lettres, et les résultats pertinents apparaissent presque instantanément.

Voici ce que vous allez apprendre à coder aujourd’hui :

Le code complet de cet exemple est disponible à la fin de ce tutoriel.

    Recherche + sélection (combo moderne)

    D’un point de vue technique, il est important de comprendre qu’il n’existe pas de balise HTML native appelée “combobox” qui ferait tout cela toute seule. Une vraie combobox est une construction. Elle repose sur du HTML pour la structure, du CSS pour l’apparence et l’ergonomie, et du JavaScript pour gérer les interactions, le filtrage et la navigation au clavier.

    À quoi sert une combobox ?

    L’utilité principale d’une combobox est d’améliorer l’expérience utilisateur. Elle est particulièrement efficace lorsque la liste de choix devient longue ou complexe. Au lieu de forcer l’utilisateur à chercher visuellement, vous lui permettez de rechercher activement.

    Une combobox réduit les erreurs. Comme l’utilisateur choisit une valeur existante dans une liste contrôlée, vous évitez les fautes de frappe, les incohérences et les données imprévues. C’est un avantage énorme côté serveur, notamment lorsque vous traitez des formulaires en PHP ou que vous stockez des données en base.

    Enfin, il faut être honnête, une combobox bien conçue donne une impression de professionnalisme. Elle évoque les interfaces modernes que l’on retrouve dans les grandes applications web, ce qui inspire confiance et sérieux.

    Quand utiliser une combobox ?

    La combobox n’est pas toujours la meilleure solution, et c’est important de le préciser. Si vous avez trois ou quatre options à proposer, un simple select HTML est souvent plus simple et plus accessible.

    En revanche, dès que la liste commence à s’allonger, la combobox devient pertinente. C’est le cas pour des listes de pays, de départements, de clients, de catégories, ou encore de produits. Elle est aussi très utile lorsque l’utilisateur connaît déjà ce qu’il cherche et souhaite aller vite.

    Notre objectif dans ce tutoriel

    Nous allons coder une vraie combobox, pas une simple liste déroulante déguisée. Elle permettra à l’utilisateur de taper du texte, de voir les résultats filtrés en temps réel, de naviguer au clavier avec les flèches, de valider avec la touche Entrée, et de fermer la liste proprement.

    Nous allons utiliser un exemple concret avec une liste de pays, car c’est un cas simple et parlant. Le code sera volontairement clair, commenté et structuré pour que vous puissiez le reprendre facilement dans vos propres projets, que ce soit pour un site vitrine, un formulaire de contact ou même un site e-commerce.

    Le HTML d’une combobox : poser des bases solides

    Avant de parler de styles ou de JavaScript, il est essentiel de comprendre une chose. Une combobox bien conçue repose sur une structure HTML claire. Si la base est saine, tout le reste devient plus simple, plus lisible et plus maintenable.

    Dans notre cas, nous allons construire la combobox comme un petit bloc autonome. Elle contient un label, un champ de saisie visible, un bouton pour ouvrir la liste, une liste de résultats, et un champ caché pour stocker la valeur réelle sélectionnée. Cette séparation est volontaire et très importante.

    <div class="combobox">
      <label class="combobox-label" for="combo-input">Choisissez un pays</label>
    
      <div class="combobox-field">
        <input
          id="combo-input"
          class="combobox-input"
          type="text"
          placeholder="Tapez pour chercher..."
          autocomplete="off"
          role="combobox"
          aria-expanded="false"
          aria-controls="combo-listbox"
          aria-autocomplete="list"
        />
    
        <input type="hidden" name="pays" id="combo-value" value="" />
    
        <button class="combobox-btn" type="button" aria-label="Ouvrir la liste">
            <span class="combobox-arrow"></span>
        </button>
      </div>
    
      <ul id="combo-listbox" class="combobox-list" role="listbox"></ul>
    
    </div>

    Le conteneur principal

    Nous commençons par un conteneur global qui englobe toute la combobox.

    <div class="combobox">

    Ce div sert de point d’ancrage. Il permet de styliser l’ensemble du composant avec le CSS et de détecter facilement les clics à l’extérieur en JavaScript. C’est aussi une bonne pratique pour isoler un composant et éviter les conflits avec le reste de la page.

    Le label : un détail souvent négligé

    Juste après, on trouve le label.

    <label class="combobox-label" for="combo-input">
      Choisissez un pays
    </label>

    Le label n’est pas là uniquement pour faire joli. Il améliore l’accessibilité et permet à l’utilisateur de cliquer sur le texte pour activer le champ. L’attribut for est lié à l’id du champ de saisie, ce qui crée un lien direct entre les deux.

    C’est un détail, mais c’est typiquement ce genre de détail qui distingue un formulaire bricolé d’un formulaire professionnel.

    Le champ de saisie principal

    Le cœur de la combobox, c’est le champ input.

    <input
      id="combo-input"
      class="combobox-input"
      type="text"
      placeholder="Tapez pour chercher..."
      autocomplete="off"
    />

    Ce champ permet à l’utilisateur de taper du texte. Chaque caractère saisi servira ensuite à filtrer la liste des options. Le placeholder guide l’utilisateur et lui indique clairement ce qu’il doit faire.

    L’attribut autocomplete="off" est important. Sans lui, le navigateur pourrait afficher ses propres suggestions, ce qui viendrait interférer avec notre combobox personnalisée.

    Le champ caché : une astuce très utile

    Un peu plus loin, on trouve un input de type hidden.

    <input type="hidden" name="pays" id="combo-value" value="" />

    Ce champ ne s’affiche pas à l’écran, mais il joue un rôle clé. Il sert à stocker la valeur réelle sélectionnée, par exemple fr pour France ou be pour Belgique.

    Pourquoi ne pas utiliser directement la valeur du champ visible ? Parce que l’utilisateur peut taper n’importe quoi. Le champ caché garantit que seule une valeur valide, issue de la liste, sera envoyée lors de la soumission du formulaire.

    C’est une approche très courante et très propre dans les formulaires modernes.

    Le bouton pour ouvrir la liste

    À côté du champ de saisie, nous ajoutons un bouton.

    <button class="combobox-btn" type="button">
      <span class="combobox-arrow"></span>
    </button>

    Ce bouton permet d’ouvrir ou de fermer la liste sans avoir besoin de taper du texte. C’est une petite attention pour l’ergonomie. Certains utilisateurs préfèrent cliquer plutôt que saisir.

    Le type="button" est essentiel. Sans lui, le bouton pourrait se comporter comme un bouton de soumission dans un formulaire, ce qui serait très problématique.

    La liste des résultats

    Enfin, nous préparons la liste qui contiendra les options filtrées.

    <ul id="combo-listbox" class="combobox-list"></ul>

    Au départ, cette liste est vide. Elle sera remplie dynamiquement en JavaScript, en fonction de ce que l’utilisateur tape. Le fait d’utiliser une liste <ul> avec des <li> est logique d’un point de vue sémantique. On affiche bien une liste de choix.

    Pourquoi cette structure est importante

    Chaque élément HTML a un rôle précis. Rien n’est laissé au hasard :

    • Le champ visible gère l’interaction,
    • le champ caché sécurise la donnée,
    • le bouton améliore l’ergonomie
    • la liste sert d’interface dynamique.

    Cette organisation rend le code plus lisible, plus évolutif et plus facile à maintenir. Si demain vous souhaitez adapter cette combobox à WordPress, à un formulaire PHP ou à un projet plus complexe, vous aurez une base solide.

    Styliser une combobox avec le CSS

    À ce stade, notre combobox fonctionne déjà sur le papier, mais visuellement, elle reste très basique. C’est ici que le CSS entre en jeu. Son rôle n’est pas seulement de “faire joli”, mais aussi de rendre l’interface claire, compréhensible et agréable à utiliser.

    L’objectif n’est pas d’impressionner avec des effets complexes, mais de créer une combobox lisible, moderne et cohérente avec le reste d’un site web.

    .combobox {
      max-width: 360px;
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
    }
    
    .combobox-label {
      display: block;
      font-size: 14px;
      font-weight: 600;
      margin-bottom: 8px;
      color: #111827;
    }
    
    .combobox-field {
      position: relative;
      display: flex;
      align-items: center;
    }
    
    .combobox-input {
      width: 100%;
      height: 44px;
      padding: 10px 44px 10px 14px;
    
      font-size: 14px;
      color: #111827;
      background-color: #fff;
    
      border: 1px solid rgba(17, 24, 39, 0.14);
      border-radius: 12px;
    
      outline: none;
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }
    
    .combobox-input:focus {
      border-color: #1E1E51;
      box-shadow: 0 0 0 4px rgba(30, 30, 81, 0.18);
    }
    
    .combobox-btn {
      position: absolute;
      right: 6px;
      height: 34px;
      width: 34px;
    
      border: none;
      background: transparent;
      border-radius: 10px;
      cursor: pointer;
    
      display: grid;
      place-items: center;
      transition: background 0.2s ease;
    }
    
    .combobox-btn:hover {
      background: rgba(17, 24, 39, 0.06);
    }
    
    .combobox-arrow {
      width: 10px;
      height: 10px;
      transform: rotate(45deg);
      border-right: 2px solid rgba(17, 24, 39, 0.55);
      border-bottom: 2px solid rgba(17, 24, 39, 0.55);
    }
    
    .combobox-list {
      margin: 8px 0 0 0;
      padding: 6px;
      list-style: none;
    
      border: 1px solid rgba(17, 24, 39, 0.14);
      border-radius: 12px;
      background: #fff;
    
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
    
      max-height: 220px;
      overflow: auto;
    
      display: none;
    }
    
    .combobox-list.is-open {
      display: block;
    }
    
    .combobox-option {
      padding: 10px 10px;
      border-radius: 10px;
      cursor: pointer;
      font-size: 14px;
      color: #111827;
      transition: background 0.15s ease;
    }
    
    .combobox-option:hover {
      background: rgba(17, 24, 39, 0.06);
    }
    
    .combobox-option.is-active {
      background: rgba(30, 30, 81, 0.10);
      outline: 2px solid rgba(30, 30, 81, 0.20);
    }
    
    .combobox-empty {
      padding: 10px;
      font-size: 13px;
      color: rgba(17, 24, 39, 0.55);
    }
    
    .combobox-help {
      margin-top: 8px;
      font-size: 12px;
      color: rgba(17, 24, 39, 0.55);
      line-height: 1.4;
    }

    Le style global du composant

    Nous commençons par définir un cadre général pour la combobox.

    .combobox {
      max-width: 360px;
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
    }

    La largeur maximale permet d’éviter que la combobox ne s’étire excessivement sur de grands écrans. Quant à la police, elle s’appuie sur les polices système, ce qui garantit un rendu propre, rapide à charger et cohérent avec l’environnement de l’utilisateur.

    C’est un choix volontairement sobre. Une interface réussie est souvent une interface qui sait rester discrète.

    Le label : lisibilité avant tout

    Le label doit être visible sans être envahissant.

    .combobox-label {
      display: block;
      font-size: 14px;
      font-weight: 600;
      margin-bottom: 8px;
      color: #111827;
    }

    Le display: block force le label à s’afficher sur sa propre ligne. La taille de police reste modeste, mais le poids légèrement renforcé permet de bien identifier l’intitulé du champ. Le contraste est suffisant pour être lisible, sans être agressif.

    Le champ de saisie principal

    C’est l’élément que l’utilisateur va le plus manipuler, il mérite donc une attention particulière.

    .combobox-input {
      width: 100%;
      height: 44px;
      padding: 10px 44px 10px 14px;
      font-size: 14px;
      color: #111827;
      background-color: #fff;
      border: 1px solid rgba(17, 24, 39, 0.14);
      border-radius: 12px;
      outline: none;
      transition: border-color 0.2s ease, box-shadow 0.2s ease;
    }

    La hauteur de 44 pixels est un standard confortable, aussi bien à la souris qu’au tactile. Le padding droit est volontairement plus large pour laisser la place au bouton avec la flèche.

    Le border-radius apporte une touche moderne, tandis que les transitions adoucissent les changements d’état. Rien n’est brutal, tout est fluide.

    L’état de focus : un repère essentiel

    Quand l’utilisateur clique ou navigue au clavier, il doit savoir où il se trouve.

    .combobox-input:focus {
      border-color: #1E1E51;
      box-shadow: 0 0 0 4px rgba(30, 30, 81, 0.18);
    }

    Le changement de couleur de la bordure et l’ombre légère créent un halo visuel clair. C’est à la fois esthétique et fonctionnel. L’utilisateur comprend immédiatement que le champ est actif.

    C’est un point souvent négligé, mais un bon focus améliore énormément l’accessibilité et le confort d’utilisation.

    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 ?

    Le bouton et la flèche

    Le bouton permet d’ouvrir la liste sans taper de texte. Il doit être visible, mais discret.

    .combobox-btn {
      position: absolute;
      right: 6px;
      height: 34px;
      width: 34px;
      border: none;
      background: transparent;
      border-radius: 10px;
      cursor: pointer;
      display: grid;
      place-items: center;
    }

    Le positionnement absolu permet de placer le bouton à l’intérieur du champ. Le display: grid avec place-items: center est une astuce simple et efficace pour centrer la flèche.

    La flèche elle-même est créée en CSS.

    .combobox-arrow {
      width: 10px;
      height: 10px;
      transform: rotate(45deg);
      border-right: 2px solid rgba(17, 24, 39, 0.55);
      border-bottom: 2px solid rgba(17, 24, 39, 0.55);
    }

    Pas besoin d’image ici. Deux bordures et une rotation suffisent à dessiner une flèche nette et légère. C’est plus flexible, plus performant et plus facile à adapter.

    La liste déroulante

    La liste est cachée par défaut et s’affiche uniquement lorsque nécessaire.

    .combobox-list {
      margin-top: 8px;
      padding: 6px;
      background: #fff;
      border: 1px solid rgba(17, 24, 39, 0.14);
      border-radius: 12px;
      box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
      max-height: 220px;
      overflow: auto;
      display: none;
    }

    Le max-height combiné à overflow: auto permet de gérer les longues listes sans casser la mise en page. L’ombre portée donne une impression de profondeur, ce qui aide l’utilisateur à comprendre que la liste est au-dessus du contenu.

    Quand la liste est ouverte, une simple classe suffit.

    .combobox-list.is-open {
      display: block;
    }

    C’est le JavaScript qui ajoutera ou retirera cette classe.

    Les options et leur état actif

    Chaque option doit être clairement identifiable.

    .combobox-option {
      padding: 10px;
      border-radius: 10px;
      cursor: pointer;
      transition: background 0.15s ease;
    }

    Au survol ou lors de la navigation clavier, l’option active est mise en avant.

    .combobox-option.is-active {
      background: rgba(30, 30, 81, 0.10);
    }

    Ce retour visuel est essentiel. Il rassure l’utilisateur et rend la navigation intuitive, même sans souris.

    Donner vie à une combobox avec JavaScript

    Jusqu’ici, nous avons une belle interface, bien structurée et bien stylisée. Mais sans JavaScript, notre combobox resterait figée. Elle ne filtrerait rien, ne réagirait pas au clavier et ne saurait pas quoi faire lorsque l’utilisateur clique.

    Le rôle du JavaScript est donc central. Il va écouter les actions de l’utilisateur, manipuler le DOM, filtrer les données et synchroniser les valeurs.

    <script>
      (function () {
        const data = [
      { value: "de", label: "Allemagne" },
      { value: "dz", label: "Algérie" },
      { value: "ar", label: "Argentine" },
      { value: "au", label: "Australie" },
      { value: "at", label: "Autriche" }
    ];
    
    
        const input = document.getElementById("combo-input");
        const hidden = document.getElementById("combo-value");
        const list = document.getElementById("combo-listbox");
        const btn = document.querySelector(".combobox-btn");
    
        let filtered = [...data];
        let activeIndex = -1;
    
        function openList() {
          list.classList.add("is-open");
          input.setAttribute("aria-expanded", "true");
        }
    
        function closeList() {
          list.classList.remove("is-open");
          input.setAttribute("aria-expanded", "false");
          activeIndex = -1;
          updateActiveOption();
        }
    
        function renderList(items) {
          list.innerHTML = "";
    
          if (!items.length) {
            const li = document.createElement("li");
            li.className = "combobox-empty";
            li.textContent = "Aucun résultat";
            list.appendChild(li);
            return;
          }
    
          items.forEach((item, index) => {
            const li = document.createElement("li");
            li.className = "combobox-option";
            li.textContent = item.label;
            li.setAttribute("role", "option");
            li.dataset.value = item.value;
            li.dataset.index = index;
    
            li.addEventListener("mousedown", (e) => {
              // mousedown pour éviter blur avant click
              e.preventDefault();
              selectItem(index);
            });
    
            list.appendChild(li);
          });
    
          updateActiveOption();
        }
    
        function filterList(query) {
          const q = query.trim().toLowerCase();
    
          filtered = data.filter((item) =>
            item.label.toLowerCase().includes(q)
          );
    
          activeIndex = filtered.length ? 0 : -1;
          renderList(filtered);
          openList();
        }
    
        function selectItem(index) {
          const item = filtered[index];
          if (!item) return;
    
          input.value = item.label;
          hidden.value = item.value;
    
          closeList();
        }
    
        function updateActiveOption() {
          const options = list.querySelectorAll(".combobox-option");
          options.forEach((opt) => opt.classList.remove("is-active"));
    
          if (activeIndex >= 0 && options[activeIndex]) {
            options[activeIndex].classList.add("is-active");
            options[activeIndex].scrollIntoView({ block: "nearest" });
          }
        }
    
        input.addEventListener("focus", () => {
          filterList(input.value);
        });
    
        input.addEventListener("input", () => {
          filterList(input.value);
          // si l’utilisateur modifie, on vide la valeur réelle
          hidden.value = "";
        });
    
        input.addEventListener("keydown", (e) => {
          const isOpen = list.classList.contains("is-open");
          const optionsCount = filtered.length;
    
          if (e.key === "ArrowDown") {
            e.preventDefault();
            if (!isOpen) openList();
            if (!optionsCount) return;
    
            activeIndex = (activeIndex + 1) % optionsCount;
            updateActiveOption();
          }
    
          if (e.key === "ArrowUp") {
            e.preventDefault();
            if (!isOpen) openList();
            if (!optionsCount) return;
    
            activeIndex = (activeIndex - 1 + optionsCount) % optionsCount;
            updateActiveOption();
          }
    
          if (e.key === "Enter") {
            if (!isOpen) return;
            e.preventDefault();
            if (activeIndex >= 0) selectItem(activeIndex);
          }
    
          if (e.key === "Escape") {
            closeList();
          }
        });
    
        btn.addEventListener("click", () => {
          const isOpen = list.classList.contains("is-open");
          if (isOpen) {
            closeList();
          } else {
            filterList(input.value);
          }
          input.focus();
        });
    
        document.addEventListener("click", (e) => {
          const root = document.querySelector(".combobox");
          if (!root.contains(e.target)) closeList();
        });
    
        // Initialisation
        renderList(data);
      })();
    </script>

    La liste de données

    Tout commence par les données que l’on souhaite afficher.

    const data = [
      { value: "fr", label: "France" },
      { value: "be", label: "Belgique" },
      { value: "es", label: "Espagne" },
      { value: "pt", label: "Portugal" },
      { value: "mc", label: "Monaco" },
      { value: "lu", label: "Luxembourg" },
      ...
    ];

    Chaque élément contient une valeur technique et un label lisible. Cette séparation est très importante :

    • label est ce que l’utilisateur voit,
    • value est ce qui sera envoyé au serveur.

    C’est exactement le même principe qu’un select HTML classique, mais appliqué à une combobox personnalisée.

    Récupérer les éléments du DOM

    Avant toute chose, il faut récupérer les éléments HTML avec lesquels nous allons interagir.

    const input = document.getElementById("combo-input");
    const hidden = document.getElementById("combo-value");
    const list = document.getElementById("combo-listbox");
    const btn = document.querySelector(".combobox-btn");

    Ces constantes représentent les briques essentielles de la combobox. Le champ visible, le champ caché, la liste des options et le bouton.

    Cette étape est fondamentale. Sans ces références, JavaScript ne peut rien manipuler.

    Générer la liste dynamiquement

    La liste des options n’est jamais écrite en dur dans le HTML. Elle est générée dynamiquement.

    function renderList(items) {
      list.innerHTML = "";
    
      items.forEach((item, index) => {
        const li = document.createElement("li");
        li.className = "combobox-option";
        li.textContent = item.label;
        li.dataset.value = item.value;
        li.dataset.index = index;
    
        li.addEventListener("mousedown", (e) => {
          e.preventDefault();
          selectItem(index);
        });
    
        list.appendChild(li);
      });
    }

    On commence par vider la liste, puis on recrée chaque option à partir des données filtrées. Le choix de mousedown plutôt que click est volontaire. Cela évite que le champ perde le focus avant la sélection, ce qui pourrait fermer la liste trop tôt.

    Chaque option connaît son index et sa valeur, ce qui facilite la sélection.

    Filtrer les résultats en temps réel

    La recherche est déclenchée dès que l’utilisateur tape dans le champ.

    function filterList(query) {
      const q = query.toLowerCase();
    
      const filtered = data.filter(item =>
        item.label.toLowerCase().includes(q)
      );
    
      renderList(filtered);
      openList();
    }

    Le principe est simple. On transforme le texte saisi en minuscules, puis on compare avec les labels. Si le label contient la chaîne saisie, il est conservé.

    Cette logique est volontairement lisible. Elle peut être optimisée plus tard, mais pour un débutant, la clarté prime.

    Ouvrir et fermer la liste

    L’ouverture et la fermeture de la liste reposent uniquement sur une classe CSS.

    function openList() {
      list.classList.add("is-open");
    }
    
    function closeList() {
      list.classList.remove("is-open");
    }

    Cette séparation entre logique et présentation est très saine. Le JavaScript décide quand ouvrir, le CSS décide comment afficher.

    Sélectionner une valeur

    Quand l’utilisateur clique sur une option ou valide au clavier, il faut mettre à jour les champs.

    function selectItem(index) {
      const item = data[index];
      if (!item) return;
    
      input.value = item.label;
      hidden.value = item.value;
      closeList();
    }

    Le champ visible affiche le texte lisible, tandis que le champ caché stocke la valeur réelle. Cette étape est cruciale pour la soumission du formulaire.

    Gérer le clavier

    Une vraie combobox se doit d’être utilisable au clavier.

    input.addEventListener("keydown", (e) => {
      if (e.key === "ArrowDown") {
        e.preventDefault();
        openList();
      }
    
      if (e.key === "Escape") {
        closeList();
      }
    });

    Dans une version avancée, on peut gérer la navigation complète avec les flèches et la touche Entrée. L’essentiel ici est de comprendre que chaque touche peut déclencher un comportement spécifique.

    Fermer la combobox au clic extérieur

    Enfin, il est important de fermer la liste lorsque l’utilisateur clique ailleurs.

    document.addEventListener("click", (e) => {
      if (!document.querySelector(".combobox").contains(e.target)) {
        closeList();
      }
    });

    Ce comportement paraît évident pour l’utilisateur, mais il doit être explicitement codé. C’est souvent ce genre de détail qui donne une impression de finition.

    Pourquoi cette logique fonctionne bien

    Cette combobox repose sur des principes simples. Une structure claire, un CSS lisible, et un JavaScript découplé en petites fonctions. Rien de magique, rien de caché.

    C’est exactement cette approche qui vous permettra de comprendre, modifier et améliorer votre code sans le casser.

    Aller plus loin avec une combobox

    À ce stade, vous avez entre les mains une combobox fonctionnelle, compréhensible et adaptée à de nombreux cas concrets. Ce n’est pas un gadget, c’est un vrai composant d’interface que vous pouvez intégrer dans vos formulaires dès aujourd’hui.

    Cependant, il est important de comprendre qu’une combobox n’est jamais figée. Selon le contexte, vous pouvez la faire évoluer. Par exemple, vous pouvez connecter la recherche à une base de données via une requête Ajax, afin de gérer des milliers d’entrées sans alourdir la page. Vous pouvez également ajouter la sélection multiple, afficher des icônes, ou encore mettre en surbrillance les caractères recherchés.

    Il est aussi possible d’améliorer encore l’accessibilité en ajoutant des rôles ARIA plus poussés. Cela permet aux lecteurs d’écran de mieux interpréter la combobox. Ce point est souvent ignoré par les débutants, mais il devient essentiel dès que l’on travaille sur des projets professionnels ou institutionnels.

    Enfin, retenez une chose importante. Une bonne combobox n’est pas celle qui fait le plus d’effets visuels, mais celle qui se fait oublier. Si l’utilisateur comprend immédiatement comment l’utiliser, alors votre travail est réussi.

    Les erreurs courantes à éviter

    Lorsque l’on débute, certaines erreurs reviennent souvent. La première consiste à vouloir tout faire en JavaScript sans réfléchir à la structure HTML. Or, sans une base claire, le code devient vite difficile à maintenir.

    Une autre erreur fréquente est d’oublier le clavier. Une combobox utilisable uniquement à la souris est frustrante pour beaucoup d’utilisateurs. Même une gestion simple des touches améliore énormément l’expérience.

    Enfin, il faut éviter de confondre combobox et champ libre. Si la valeur doit être contrôlée, utilisez toujours un champ caché pour stocker la donnée réelle. Cela vous évitera bien des problèmes côté serveur.

    Le code complet : HTML + CSS + JS

    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>New Document</title>
        <style>
          /* ===== ComboBox CSS (select custom minimaliste) ===== */
          /* ===== VRAIE COMBOBOX (autocomplete) ===== */
          .combobox {
            max-width: 360px;
            font-family:
              system-ui,
              -apple-system,
              Segoe UI,
              Roboto,
              Arial,
              sans-serif;
          }
    
          .combobox-label {
            display: block;
            font-size: 14px;
            font-weight: 600;
            margin-bottom: 8px;
            color: #111827;
          }
    
          .combobox-field {
            position: relative;
            display: flex;
            align-items: center;
          }
    
          .combobox-input {
            width: 100%;
            height: 44px;
            padding: 10px 44px 10px 14px;
    
            font-size: 14px;
            color: #111827;
            background-color: #fff;
    
            border: 1px solid rgba(17, 24, 39, 0.14);
            border-radius: 12px;
    
            outline: none;
            transition:
              border-color 0.2s ease,
              box-shadow 0.2s ease;
          }
    
          .combobox-input:focus {
            border-color: #1e1e51;
            box-shadow: 0 0 0 4px rgba(30, 30, 81, 0.18);
          }
    
          .combobox-btn {
            position: absolute;
            right: 6px;
            height: 34px;
            width: 34px;
    
            border: none;
            background: transparent;
            border-radius: 10px;
            cursor: pointer;
    
            display: grid;
            place-items: center;
            transition: background 0.2s ease;
          }
    
          .combobox-btn:hover {
            background: rgba(17, 24, 39, 0.06);
          }
    
          .combobox-arrow {
            width: 10px;
            height: 10px;
            transform: rotate(45deg);
            border-right: 2px solid rgba(17, 24, 39, 0.55);
            border-bottom: 2px solid rgba(17, 24, 39, 0.55);
          }
    
          .combobox-list {
            margin: 8px 0 0 0;
            padding: 6px;
            list-style: none;
    
            border: 1px solid rgba(17, 24, 39, 0.14);
            border-radius: 12px;
            background: #fff;
    
            box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08);
    
            max-height: 220px;
            overflow: auto;
    
            display: none;
          }
    
          .combobox-list.is-open {
            display: block;
          }
    
          .combobox-option {
            padding: 10px 10px;
            border-radius: 10px;
            cursor: pointer;
            font-size: 14px;
            color: #111827;
            transition: background 0.15s ease;
          }
    
          .combobox-option:hover {
            background: rgba(17, 24, 39, 0.06);
          }
    
          .combobox-option.is-active {
            background: rgba(30, 30, 81, 0.1);
            outline: 2px solid rgba(30, 30, 81, 0.2);
          }
    
          .combobox-empty {
            padding: 10px;
            font-size: 13px;
            color: rgba(17, 24, 39, 0.55);
          }
    
          .combobox-help {
            margin-top: 8px;
            font-size: 12px;
            color: rgba(17, 24, 39, 0.55);
            line-height: 1.4;
          }
        </style>
      </head>
      <body>
        <h1>Hello World</h1>
        <div class="combobox">
          <label class="combobox-label" for="combo-input">Choisissez un pays</label>
    
          <div class="combobox-field">
            <input
              id="combo-input"
              class="combobox-input"
              type="text"
              placeholder="Tapez pour chercher..."
              autocomplete="off"
              role="combobox"
              aria-expanded="false"
              aria-controls="combo-listbox"
              aria-autocomplete="list"
            />
    
            <input type="hidden" name="pays" id="combo-value" value="" />
    
            <button class="combobox-btn" type="button" aria-label="Ouvrir la liste">
              <span class="combobox-arrow"></span>
            </button>
          </div>
    
          <ul id="combo-listbox" class="combobox-list" role="listbox"></ul>
    
        </div>
    
        <script>
          (function () {
            const data = [
              { value: "fr", label: "France" },
              { value: "be", label: "Belgique" },
              { value: "es", label: "Espagne" },
              { value: "pt", label: "Portugal" },
              { value: "mc", label: "Monaco" },
              { value: "lu", label: "Luxembourg" },
              { value: "it", label: "Italie" },
              { value: "de", label: "Allemagne" },
              { value: "ch", label: "Suisse" },
            ];
    
            const input = document.getElementById("combo-input");
            const hidden = document.getElementById("combo-value");
            const list = document.getElementById("combo-listbox");
            const btn = document.querySelector(".combobox-btn");
    
            let filtered = [...data];
            let activeIndex = -1;
    
            function openList() {
              list.classList.add("is-open");
              input.setAttribute("aria-expanded", "true");
            }
    
            function closeList() {
              list.classList.remove("is-open");
              input.setAttribute("aria-expanded", "false");
              activeIndex = -1;
              updateActiveOption();
            }
    
            function renderList(items) {
              list.innerHTML = "";
    
              if (!items.length) {
                const li = document.createElement("li");
                li.className = "combobox-empty";
                li.textContent = "Aucun résultat";
                list.appendChild(li);
                return;
              }
    
              items.forEach((item, index) => {
                const li = document.createElement("li");
                li.className = "combobox-option";
                li.textContent = item.label;
                li.setAttribute("role", "option");
                li.dataset.value = item.value;
                li.dataset.index = index;
    
                li.addEventListener("mousedown", (e) => {
                  // mousedown pour éviter blur avant click
                  e.preventDefault();
                  selectItem(index);
                });
    
                list.appendChild(li);
              });
    
              updateActiveOption();
            }
    
            function filterList(query) {
              const q = query.trim().toLowerCase();
    
              filtered = data.filter((item) =>
                item.label.toLowerCase().includes(q),
              );
    
              activeIndex = filtered.length ? 0 : -1;
              renderList(filtered);
              openList();
            }
    
            function selectItem(index) {
              const item = filtered[index];
              if (!item) return;
    
              input.value = item.label;
              hidden.value = item.value;
    
              closeList();
            }
    
            function updateActiveOption() {
              const options = list.querySelectorAll(".combobox-option");
              options.forEach((opt) => opt.classList.remove("is-active"));
    
              if (activeIndex >= 0 && options[activeIndex]) {
                options[activeIndex].classList.add("is-active");
                options[activeIndex].scrollIntoView({ block: "nearest" });
              }
            }
    
            input.addEventListener("focus", () => {
              filterList(input.value);
            });
    
            input.addEventListener("input", () => {
              filterList(input.value);
              // si l’utilisateur modifie, on vide la valeur réelle
              hidden.value = "";
            });
    
            input.addEventListener("keydown", (e) => {
              const isOpen = list.classList.contains("is-open");
              const optionsCount = filtered.length;
    
              if (e.key === "ArrowDown") {
                e.preventDefault();
                if (!isOpen) openList();
                if (!optionsCount) return;
    
                activeIndex = (activeIndex + 1) % optionsCount;
                updateActiveOption();
              }
    
              if (e.key === "ArrowUp") {
                e.preventDefault();
                if (!isOpen) openList();
                if (!optionsCount) return;
    
                activeIndex = (activeIndex - 1 + optionsCount) % optionsCount;
                updateActiveOption();
              }
    
              if (e.key === "Enter") {
                if (!isOpen) return;
                e.preventDefault();
                if (activeIndex >= 0) selectItem(activeIndex);
              }
    
              if (e.key === "Escape") {
                closeList();
              }
            });
    
            btn.addEventListener("click", () => {
              const isOpen = list.classList.contains("is-open");
              if (isOpen) {
                closeList();
              } else {
                filterList(input.value);
              }
              input.focus();
            });
    
            document.addEventListener("click", (e) => {
              const root = document.querySelector(".combobox");
              if (!root.contains(e.target)) closeList();
            });
    
            // Initialisation
            renderList(data);
          })();
        </script>
      </body>
    </html>

    Apprendre à coder une combobox en CSS et JavaScript, ce n’est pas seulement apprendre à créer un composant précis. C’est surtout comprendre comment dialoguent le HTML, le CSS et le JavaScript dans un projet concret. Chacun a son rôle, et aucun ne doit empiéter inutilement sur l’autre.

    Ce type de composant est un excellent exercice pour progresser. Il vous oblige à réfléchir à l’expérience utilisateur, à la lisibilité du code, et à la robustesse de votre logique. En le maîtrisant, vous franchissez un vrai cap dans votre apprentissage du développement front-end.

    Si vous deviez retenir une seule chose, ce serait celle-ci :

    Le web moderne n’est pas une accumulation d’effets, mais une succession de petites attentions.

    Une combobox bien pensée en fait partie. Prenez le temps d’expérimenter, de casser le code, de l’améliorer. C’est exactement comme cela que l’on apprend vraiment à coder.