Drupal : ajouter des icônes à ses liens de menu custom dans la navigation d'admin

Le nouveau module Navigation du core (la sidebar verticale qui remplace la vieille toolbar horizontale) affiche une icône devant chaque entrée de premier niveau. 

Pour les entrées du core, tout est prévu. Mais dès qu'un module custom ajoute son propre lien de niveau 1 (une section « métier », un raccourci « Vider le cache »...), la sidebar se rabat sur un pis-aller : les deux premières lettres du titre dans un carré. Fonctionnel, mais pas très flatteur.

Nous allons voir comment avoir des éléments de menus custom avec leurs icônes :

icone custom sur menu navigation

Comment que ça marche dans le core ?

Au moment de construire l'arbre de la sidebar, NavigationMenuLinkTree fabrique pour chaque lien de premier niveau un render array d'icône avec des valeurs par défaut, puis fusionne par-dessus l'option icon du lien si elle existe :

// web/core/modules/navigation/src/Menu/NavigationMenuLinkTree.php (extrait simplifié)
$icon_defaults = [
  'pack_id' => 'navigation',
  'icon_id' => $plugin_class,
  'settings' => [
    'class' => 'toolbar-button__icon',
    'size' => 20,
  ],
];
$icon = NestedArray::mergeDeep($icon_defaults, $url->getOption('icon') ?? []);

Trois choses à retenir de ces quelques lignes :

  • par défaut, l'icône est cherchée dans le pack navigation du core, avec un identifiant dérivé du plugin id du lien. C'est comme ça que les entrées du core ont leurs icônes... et que les nôtres n'en ont pas : aucun SVG du pack core ne s'appelle comme notre lien, d'où le repli sur les initiales ;
  • un lien de menu peut fournir sa propre définition via $url->getOption('icon') : pack, icône, et même les settings ;
  • le core transmet deux settings au pack : une classe CSS (toolbar-button__icon) et une taille (20). Nous allons y revenir.

Déclarer l'icône sur le lien de menu

Les « options » d'un lien de menu se déclarent directement dans le *.links.menu.yml du module. Il suffit donc d'y poser une clé options.icon qui pointe vers n'importe quel pack d'icônes installé, celui du projet comme une bibliothèque tierce :

# mon_module.links.menu.yml
mon_module.admin:
  title: 'Popote Minute'
  route_name: mon_module.admin
  parent: system.admin
  weight: -15
  options:
    icon:
      pack_id: mon_pack
      icon_id: toque

mon_module.flush_caches:
  title: 'Vider le cache'
  route_name: mon_module.flush_caches
  parent: system.admin
  weight: -16
  options:
    icon:
      pack_id: iconoir
      icon_id: trash

Ici la toque vient du pack maison du projet et la poubelle de la bibliothèque Iconoir. La création de ces packs (fichier *.icons.yml, normalisation des SVG en currentColor, settings...) est détaillée dans l'article précédent sur l'API Icon et UI Icons ; celui-ci en est la suite directe.

Un drush cr plus tard (les définitions de liens de menu sont en cache, comme le reste), les icônes apparaissent dans la sidebar.

Le réglage class du pack

Petit détail qui a son importance : le core transmet settings: ['class' => 'toolbar-button__icon', 'size' => 20] au pack. Cette classe porte le dimensionnement et l'alignement de l'icône dans le bouton de la sidebar. Si le template du pack l'ignore, l'icône sort du gabarit.

Un pack maison doit donc déclarer un setting class et le reporter sur la racine du SVG :

  settings:
    class:
      title: 'Class'
      description: 'Classes CSS supplémentaires, transmises notamment par le module Navigation.'
      type: 'string'

Et dans le template du pack :

.addClass('icon', 'icon--' ~ icon_id|clean_class, class|default(''))

Pour récapituler, voici l'intégralité de mon fichier mon_theme.icons.yaml

mon_pack:
  enabled: true
  label: 'Popote Icons'
  description: 'Icônes SVG du projet Popote Minute.'
  version: '1.0.0'
  license:
    name: 'Proprietary'
    url: 'https://popote-minute.fr'
    gpl-compatible: false
  extractor: svg
  config:
    sources:
      - icons/*.svg
  settings:
    size:
      title: 'Size'
      description: "Largeur et hauteur en pixels. Sans valeur, le CSS du contexte décide."
      type: 'integer'
    color:
      title: 'Color'
      description: "Couleur de l'icône (remplissage ou trait selon l'icône). Défaut : couleur du texte."
      type: 'string'
      format: 'color'
    stroke:
      title: 'Stroke width'
      description: "Épaisseur du trait des icônes filaires (arrow, chevron…)."
      type: 'number'
      default: 2
      minimum: 1
      maximum: 3
      multipleOf: 0.1
    class:
      title: 'Class'
      description: "Classe(s) CSS ajoutée(s) au SVG — le core Navigation s'en sert (toolbar-button__icon)."
      type: 'string'
  template: >-
    {% if color %}<span style="color: {{ color }};">{% endif %}
    <svg{{
      attributes
        .setAttribute('xmlns', 'http://www.w3.org/2000/svg')
        .setAttribute('viewBox', attributes.viewBox|default('0 0 24 24'))
        .setAttribute('stroke-width', stroke|default(2))
        .setAttribute('aria-hidden', 'true')
        .setAttribute('focusable', 'false')
        .addClass('icon', 'icon--' ~ icon_id|clean_class, class|default(''))
    }}{% if size %} width="{{ size }}" height="{{ size }}"{% endif %}>{{ content }}</svg>
    {% if color %}</span>{% endif %}
iconoir:
  enabled: true
  label: "Iconoir"
  description: "Iconoir is an open-source library with 1500+ unique SVG icons, designed on a 24x24 pixels grid."
  links:
    - https://github.com/iconoir-icons/iconoir
  version: 7.11.0
  license:
    name: MIT
    url: https://github.com/iconoir-icons/iconoir/blob/main/LICENSE
    gpl-compatible: true
  extractor: svg
  config:
    sources:
      - /libraries/node_modules/iconoir/icons/{group}/{icon_id}.svg
  settings:
    size:
      title: "Size"
      type: "integer"
      default: 32
    color:
      title: "Color"
      type: "string"
      format: "color"
    stroke:
      title: "Stroke width"
      type: "number"
      default: 1.5
      minimum: 1.0
      maximum: 2.0
      multipleOf: 0.1
    class:
      title: "Class"
      description: "Classe(s) CSS ajoutée(s) au SVG — le core Navigation s'en sert (toolbar-button__icon)."
      type: "string"
  template: >-
    {% if color is defined %}
    <span style="color: {{ color }};">
    {% endif %}
    <svg xmlns="http://www.w3.org/2000/svg"{{
        attributes
          .setAttribute('viewBox', attributes.viewBox|default('0 0 24 24'))
          .setAttribute('width', size|default('32'))
          .setAttribute('height', size|default('32'))
          .setAttribute('fill', 'none')
          .setAttribute('stroke-width', stroke|default('1.5'))
          .setAttribute('aria-hidden', 'true')
          .addClass('icon', class|default(''))
    }}>
      {{ content }}
    </svg>
    {% if color is defined %}
    </span>
    {% endif %}

Le setting size est aussi transmis : si le pack le déclare (c'est le cas des exemples de l'article précédent), l'icône reçoit son width/height de 20 pixels sans rien faire de plus.

Écrit par Kevin Gautreau — développeur & formateur PHP/Drupal freelance en Auvergne. me contacter

Contenus en rapport

article · 4 Aoû 2026 Drupal : créer son propre pack d'icônes SVG avec l'API Icon et UI Icons Depuis Drupal 11.1, le core embarque une API Icon : on déclare un pack d'icônes dans un fichier YAML, et une fonction Twig les rend n'importe où (templates…

Commentaires (0)