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 icon() les rend n'importe où (templates, SDC, render arrays). Côté back-office, le module contrib UI Icons ajoute ce que le core n'a pas : un type de champ, un sélecteur visuel et une page qui liste tous les packs installés.

Il est possible d'utiliser les kits d'icones classiques avec UI Icons, mais aussi se faire son kit personnalisé à partir de fichiers SVG. C'est ce que l'on va voir ici avec un kit personnalisé et l'intégration de la librairie opensource Iconoir.

Les modules

Nous allons commencer par installer UI Icons et activer les sous-modules

composer require drupal/ui_icons
drush en ui_icons ui_icons_field ui_icons_picker ui_icons_library
  • ui_icons_field : un type de champ « icône », qui stocke la référence sous la forme pack_id:icon_id ;
  • ui_icons_picker : le sélecteur visuel dans les formulaires (une modale avec recherche, bien plus agréable qu'un select) ;
  • ui_icons_library : une page d'inventaire sous /admin/appearance/ui/icons, pratique pour vérifier qu'un pack est bien découvert et montrer le catalogue au client.

Déclarer le pack dans le thème

Un pack se déclare dans un fichier <extension>.icons.yml, à la racine d'un module ou d'un thème.

Dans web/themes/custom/mon_theme/mon_theme.icons.yml :

mon_pack:
  enabled: true
  label: 'Mes icônes'
  description: 'Icônes SVG du projet.'
  version: '1.0.0'
  license:
    name: 'Proprietary'
    url: 'https://exemple.fr'
    gpl-compatible: false
  extractor: svg
  config:
    sources:
      - icons/*.svg

Les SVG sont simplement déposés dans web/themes/custom/mon_theme/icons/, le nom du fichier devient l'identifiant de l'icône : toque.svg donne mon_pack:toque.

Deux choses à savoir :

  • l'identifiant du pack est la clé YAML (mon_pack), pas le nom de l'extension qui le porte. On peut donc déplacer la déclaration d'un module vers un thème (ou l'inverse) sans casser une seule référence pack_id:icon_id stockée en base.
  • les packs sont mis en cache de façon agressive : après tout ajout d'icône ou modification du YAML, drush cr. Mais bon, si vous faite du drupal depuis quelques temps, vous avez l'habitude.

Normaliser les sources SVG

Les SVG livré dans le cadre de notre projet n'était pas tip top... La règle : aucune couleur ni épaisseur de trait en dur dans les sources. La peinture est portée par des attributs currentColor sur les tracés :

  • icônes pleines : fill="currentColor" ;
  • icônes filaires : fill="none" stroke="currentColor", sans stroke-width (il sera posé par le template).
<!-- Icône filaire normalisée -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <path d="M13.145 7.907c..." fill="none" stroke="currentColor"
        stroke-linecap="round" stroke-linejoin="round"/>
</svg>

<!-- Icône pleine normalisée -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
  <path d="M20.863 7.782c..." fill="currentColor"/>
</svg>

Grâce à ça, l'icône hérite de la couleur du texte ambiant par défaut, et les réglages color et stroke du pack touchent toutes les icônes sans exception. Un export Illustrator ou Figma brut ne respecte jamais ces règles : il faut repasser sur chaque fichier livré, une fois, à l'entrée dans le pack.

On conserve en revanche le viewBox d'origine de chaque fichier, y compris pour les icônes non carrées (notre vague fait du 53,761 × 41,711). Le template s'en servira.

Les réglages : size, color, stroke

Un pack peut déclarer des settings, décrits en JSON Schema. UI Icons en fait un mini-formulaire dans le picker et le type de champ, et les valeurs arrivent dans le template comme variables Twig :

  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. Défaut : couleur du texte."
      type: 'string'
      format: 'color'
    stroke:
      title: 'Stroke width'
      description: 'Épaisseur du trait des icônes filaires.'
      type: 'number'
      default: 2
      minimum: 1
      maximum: 3
      multipleOf: 0.1

Un choix délibéré ici : size n'a pas de valeur par défaut. Nos composants dimensionnent leurs icônes en CSS (em, variables, media queries), un width/height en dur n'apporterait que des prises de tête.

Le template

Chaque pack fournit son template de rendu, directement dans le YAML. C'est lui qui reçoit content (le contenu extrait du SVG), attributes, icon_id et les settings :

  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)
    }}{% if size %} width="{{ size }}" height="{{ size }}"{% endif %}>{{ content }}</svg>
    {% if color %}</span>{% endif %}

Ligne par ligne, les points qui comptent :

  • le viewBox vient du fichier source : l'extracteur svg expose les attributs de la racine du SVG dans attributes, on reprend donc attributes.viewBox avec un repli sur 0 0 24 24. C'est ce qui permet aux icônes non carrées de garder leurs proportions sans réglage supplémentaire ;
  • aria-hidden="true" et focusable="false" : nos icônes sont décoratives, leur nom accessible vient toujours du texte ou de l'aria-label qui les accompagne. focusable="false" les sort de l'ordre de tabulation sur les moteurs qui y mettent encore les SVG ;
  • stroke-width posé sur la racine : comme les sources n'en portent pas, une seule valeur pilote toutes les icônes filaires, et n'a aucun effet sur les pleines ;
  • la classe icon--<id> dans le cas où l'on veut ajouter du style CSS au cas par cas.

À l'usage

Partout en Twig, via la fonction du core :

{{ icon('mon_pack', 'poele') }}
{{ icon('mon_pack', 'arrow', { size: 32, color: '#2e5941' }) }}

En PHP, via un render array :

$build['icon'] = [
  '#type' => 'icon',
  '#pack_id' => 'mon_pack',
  '#icon_id' => 'poele',
  '#settings' => ['size' => 32],
];

Et côté contenu, le champ de ui_icons_field stocke la valeur sous forme pack_id:icon_id. Dans un composant SDC qui reçoit cette valeur en via une propriété on peut splitter : 

{% set icon_parts = icon|split(':') %}
{{ icon(icon_parts[0], icon_parts[1]) }}

Le contributeur choisit son icône dans le picker, avec aperçu et recherche, et peut piocher indifféremment dans le pack maison ou dans Iconoir : les packs se cumulent.

Installation et utilisation d'une librairie tierce : Iconoir

Le même mécanisme sert à exposer une bibliothèque installée par npm, sans module dédié. Les sources acceptent des motifs avec les jokers {group} et {icon_id} :

iconoir:
  enabled: true
  label: '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

Le chemin absolu part du docroot (dans le projet courant nous les paquets npm front sont installés dans web/libraries/ non versionné).

Pour mon infra uzinasit j'ai créé des sous-modules pour iconoir, font-awesome... histoire de pouvoir activer un ou plusieurs pack directement, par site. Je me note de mettre le code ici à l'occasion.

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

Contenus en rapport

article · 6 Aoû 2026 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… article · 29 Mai 2026 Uzinasit : retour d'expérience sur mon usine à site Drupal 11 J'ai profité d'avoir un peu de temps durant l'été 2025 pour commencer à rationaliser l'ensemble de mes sites histoire de m'en simplifier la gestion Pourquoi…

Commentaires (1)

Comments

nd (non vérifié)

Bonjour, super article mais de mon côté j'ai rencontré pas mal d'embuche pour que ca fonctionne sur mon site de test (sous Wamp). Donc voici quelques compléments:

  • Sous Windows, il faut appliquer le patch du 2ème merge request du bug signalé ici : https://www.drupal.org/project/drupal/issues/3579927
  • j'ai du mettre les sources d'iconoir directement dans mon thème car Drupal n'arrivait pas à refaire les chemins si je mettais les sources dans libraries. D'ailleurs, pour récupérer les sources, la commande npm est "npm i iconoir"
  • Puis j'ai utilisé la conf ci-dessous pour mon_theme.icons.yml

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.12.1
license:
name: MIT
url: https://github.com/iconoir-icons/iconoir/blob/main/LICENSE
gpl-compatible: true
extractor: svg
config:
sources:
- 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 %}

{% endif %}

{{ content }}

{% if color is defined %}

{% endif %}