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 · 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 (0)