Utiliser et gérer des icônes

Choisir une technique d'icône

Le stockage et l'affichage des icônes sont des choix distincts. Par exemple, un sprite SVG peut fournir une icône au système d'icônes CSS ou être référencé avec <use>.

  • Système d'icônes CSS : à utiliser lorsque le CSS doit choisir quand et quelle icône afficher. Utilisez un masque pour une icône unicolore ou une image de fond pour une icône en couleur.
  • SVG avec <use> : permet de réutiliser un symbole lorsque ses options de style intégrées sont suffisantes. Le CSS ne peut pas styliser ni animer arbitrairement le contenu du SVG référencé, mais les propriétés héritées et les propriétés personnalisées peuvent être transmises lorsque le symbole est conçu pour les utiliser. Par exemple, un chemin d'accès peut utiliser fill="var(--icon-accent, currentColor)" pour permettre au CSS de colorer cette partie de l'icône.
  • SVG intégré : à utiliser lorsque vous avez besoin d'un style ou d'une animation arbitraires du contenu du SVG.
  • <img> avec des filtres CSS : évitez cette méthode pour recolorer les icônes. Les filtres ne peuvent pas appliquer de manière fiable une valeur de couleur CSS arbitraire à l'image. La recoloration est donc fastidieuse et peu flexible.

Pour vous décider, posez-vous deux questions. Le CSS doit-il choisir ou échanger l'icône (valeurs par défaut des composants, changements d'état, thèmes) ? Si oui, utilisez le système d'icônes CSS. Avez-vous besoin d'accéder à l'intérieur de l'icône au-delà de la couleur ? Si oui et que vous contrôlez la source de l'icône, concevez le symbole avec des propriétés personnalisées et utilisez <use>. Si vous ne contrôlez pas la source, intégrez le SVG. Les ensembles d'icônes tiers exposent rarement des propriétés personnalisées. Par conséquent, l'utilisation de <use> ne vous donne que la couleur et peu d'autres éléments.

Icônes intégrées ou distantes

Le système d'icônes CSS et <use> peuvent tous deux extraire des icônes d'un URI de données, d'un fichier individuel ou d'un sprite. L'intégration (URI de données ou SVG intégré) évite une requête et s'affiche dès le premier paint, mais elle alourdit le CSS ou le HTML et ne peut pas être mise en cache séparément. Elle convient donc à un petit ensemble d'icônes qui apparaissent sur chaque page. Les fichiers individuels sont mis en cache indépendamment et chargés à la demande, au coût d'une requête chacun. Un sprite est une requête pouvant être mise en cache pour l'ensemble, mais il télécharge toutes les icônes, qu'elles soient utilisées ou non, et <use> ne peut référencer qu'un sprite de la même origine. Privilégiez l'intégration pour quelques icônes principales, et un sprite ou des fichiers individuels pour les ensembles plus volumineux.

Si une page affiche des milliers d'icônes à la fois, effectuez un benchmark de la page et des éléments réels. Lors d'un test de résistance, le SVG intégré présentait un léger avantage en termes de performances à cette échelle, mais les résultats dépendent du navigateur et de l'implémentation. Test de charge des icônes SVG de Cloud Four

Approche principale : moteur d'icônes basé sur CSS

Les icônes sont des jetons de conception : chaque icône est une propriété personnalisée contenant une image, et n'importe quel élément peut en afficher une en définissant --icon-start ou --icon-end. Le moteur combine trois fonctionnalités CSS :

  • Les propriétés personnalisées enregistrées (@property) empêchent l'héritage de --icon-start et --icon-end. Ainsi, une icône définie sur un composant n'est pas transmise à ses enfants.
  • Les requêtes de style de conteneur (@container style(...)) détectent lorsqu'une icône est attribuée à un élément et l'affichent, sans modifier la classe ni le balisage.
  • Les masques CSS et currentColor affichent l'icône dans un pseudo-élément qui suit la couleur du texte de l'élément. La taille, la couleur, l'espacement et le décalage vertical sont lus à partir des propriétés --icon-style-* héritables avec des valeurs de remplacement appropriées. Ainsi, un sous-arbre entier peut être restylé à partir d'un ancêtre. Le préfixe --icon-style- empêche ces paramètres d'entrer en conflit avec les jetons d'icône eux-mêmes, qui se trouvent également sous --icon-*.

Définir des jetons de conception d'icônes

Définissez les icônes SVG et les éventuels remplacements --icon-style-* en tant que propriétés personnalisées réutilisables à l'aide d'URI de données SVG intégrés ou d'URL relatives à la racine (par exemple, url("/icons/trash.svg")). Lorsqu'une icône est chargée à partir d'un fichier SVG externe, utilisez une URL relative à la racine ou un URI de données intégré. Une URL relative utilisée via une propriété personnalisée peut être résolue par rapport à la feuille de style où l'icône est utilisée, plutôt que par rapport à la feuille de style où la propriété personnalisée est définie. Si ces feuilles de style se trouvent dans des répertoires différents, l'URL peut renvoyer vers le mauvais fichier.

Pour conserver une URL relative à la feuille de style qui définit le jeton d'icône, une propriété enregistrée est censée vous aider : la valeur calculée d'un <url> ou d'un <image> enregistré est résolue par rapport à la feuille de style qui l'a déclaré. N'enregistrez PAS le jeton en tant que <image> avec l'URL dans initial-value. Chrome et Safari résolvent les URL initial-value par rapport au document. Cela ne fonctionne donc que dans Firefox. Chrome résout également les valeurs déclarées sur :root par rapport au document, sauf si la syntaxe commence par <url>.

Lorsque les URL relatives sont inévitables, comme les feuilles de style dans différents répertoires, enregistrez le jeton avec une syntaxe commençant par <url> et déclarez l'URL réelle sur :root. Cela se résout par rapport à la feuille de style de déclaration dans tous les navigateurs :

@property --icon-caret {
  /* The syntax must start with <url>. A syntax that starts with <image> resolves against the document in Chrome. */
  syntax: "<url> | <image> | none";
  inherits: true;
  initial-value: none;
}

:root {
  /* Declared here, the URL resolves against this stylesheet */
  --icon-caret: url("icons/caret.svg");
}

Chaque icône se trouve désormais à deux endroits, et il y a beaucoup plus de code récurrent. Cela n'en vaut donc la peine que lorsqu'un projet comporte des feuilles de style dans différents répertoires et un nombre d'icônes gérable. Au-delà d'une douzaine d'icônes, enregistrez les jetons --icon-* (à l'exclusion de --icon-style-*) à partir d'un script avec CSS.registerProperty, en utilisant la même syntaxe, au lieu d'écrire chaque @property manuellement. Les propriétés enregistrées sont globales et ne fonctionnent pas dans les racines fantômes. Elles ne conviennent donc pas à tous les projets. Si les URL relatives à la racine ou les URI de données sont une option, elles sont plus simples et fonctionnent partout.

:root {
  /* Optional: override the built-in fallbacks (1em, currentColor, 0.3em, -0.02em) */
  --icon-style-size: 1em;
  --icon-style-spacing: 0.4em;
  --icon-trash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0-1 14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2L4 6h16Z'/%3E%3C/svg%3E");
  --icon-favorite: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='currentColor'%3E%3Cpath d='M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z'/%3E%3C/svg%3E");
}

Afficher des icônes à partir de jetons

Un élément affiche une icône en définissant --icon-start (début) ou --icon-end (fin) sur l'un des jetons ci-dessus. Deux éléments CSS transforment cette attribution en icône affichée.

Commencez par enregistrer --icon-start et --icon-end avec @property. La syntaxe "*" accepte n'importe quelle valeur, et inherits: false est la partie importante : sans elle, une icône définie sur un bouton serait héritée par chaque descendant, et chacun d'eux afficherait également l'icône.

@property --icon-start {
  syntax: "*";
  inherits: false;
}

@property --icon-end {
  syntax: "*";
  inherits: false;
}

Ensuite, une requête de style de conteneur détecte quand la propriété est définie sur un élément et affiche l'icône dans un pseudo-élément. Comme le pseudo-élément n'hérite pas de --icon-start (il a été enregistré comme non hérité), la première déclaration transmet explicitement la valeur avec inherit. Les déclarations restantes gèrent le dimensionnement, le masquage, l'alignement et l'espacement. Comme le remplissage est défini sur currentColor par défaut, l'icône adopte la couleur du texte de l'élément et suit ses transitions.

/* Style query for elements configuring a start icon */
@container style(--icon-start) {
  ::before {
    --icon-start: inherit; /* Forward the value inside the query block */
    content: "" / var(--icon-style-alt, "");
    display: inline-block;
    width: var(--icon-style-size, 1em);
    height: var(--icon-style-size, 1em);
    background-color: var(--icon-style-color, currentColor);
    mask: var(--icon-start) no-repeat center / contain;
    /* Center on the cap height, nudged slightly up for optical balance */
    vertical-align: calc(0.5cap - var(--icon-style-size, 1em) / 2 + var(--icon-style-voffset, -0.02em));
    flex: none;
    pointer-events: none;
    forced-color-adjust: none;
  }

  /* Automatically add spacing ONLY if the element has other text or sibling content */
  :where(:not(.icon, :empty))::before {
    margin-inline-end: var(--icon-style-spacing, 0.3em);
  }
}

Chaque déclaration a sa place :

  • vertical-align : middle s'aligne sur la moitié de la hauteur d'x, qui est visiblement basse pour la plupart des ensembles d'icônes. Le centrage sur 0.5cap maintient le centre de l'icône fixe à mesure que --icon-style-size augmente, et la petite valeur négative --icon-style-voffset compense le fait que l'œil accorde plus d'importance à l'espace au-dessus d'une icône qu'à l'espace en dessous.
  • flex: none : un pseudo-élément vide a une taille minimale de zéro. Par conséquent, dans un bouton Flex, c'est le premier élément à être réduit lorsque le libellé est long, potentiellement jusqu'à disparaître. La taille des icônes est une décision de conception, et non un élément qui doit être négocié par la pression de la mise en page.
  • pointer-events: none : empêche l'icône de capturer les clics ou de devenir la event.target.
  • forced-color-adjust: none : en mode Couleurs forcées, le navigateur remplace background-color, ce qui fait disparaître une icône masquée. Le remplissage du masque est conservé, et currentColor suit toujours la couleur de texte forcée.
  • content: "" / var(--icon-style-alt, "") : le texte alternatif pour le contenu généré permet à un contrôle composé uniquement d'une icône de récupérer son nom accessible à partir de l'icône via --icon-style-alt (voir "Contrôles composés uniquement d'une icône" ci-dessous). La valeur par défaut vide de l'attribut "alt" permet de masquer les icônes décoratives pour les technologies d'assistance.

Pour une icône de fin, suivez le même schéma avec --icon-end et ::after au lieu de --icon-start et ::before. Utilisez mask: var(--icon-end) et appliquez l'espacement avec margin-inline-start au lieu de margin-inline-end.

Éléments autonomes et icônes seules

Pour les icônes autonomes ou les boutons contenant uniquement une icône, appliquez .icon directement à l'élément vide (le bouton lui-même pour une commande contenant uniquement une icône) et définissez son icône avec --icon. Ici, --icon est la propriété de niveau supérieur. La règle .icon:empty la mappe à --icon-start pour une icône de début :

.icon:empty {
  --icon-start: var(--icon);
}

Exemples d'utilisations

Valeurs par défaut des composants dans la feuille de style

Le principal avantage des icônes en tant que propriétés personnalisées est qu'elles participent à la cascade. Définissez une icône sur un sélecteur, et chaque élément correspondant l'obtiendra, sans modification du balisage :

.button-delete {
  --icon-start: var(--icon-trash);
}

a[href^="http"]:not([href*="example.com"]) {
  --icon-end: var(--icon-external);
}
<button class="button-delete">Delete item</button>

Changer d'icône en fonction de l'état

Comme l'icône n'est qu'une propriété, un changement d'état est un remplacement CSS ordinaire. Il n'y a pas de deuxième élément à afficher ou à masquer, ni de classe à activer ou désactiver :

.button-expand {
  --icon-start: var(--icon-caret-right);
}

.button-expand[aria-expanded="true"] {
  --icon-start: var(--icon-caret-down);
}

Il en va de même pour la présentation : définissez --icon-style-size, --icon-style-color ou --icon-style-spacing sur un conteneur tel qu'une barre d'outils, et chaque icône qu'il contient sera mise à jour.

Icônes ponctuelles avec styles intégrés

Un élément qui n'est couvert par aucune règle de composant peut définir la même propriété de manière intégrée, par exemple <button style="--icon-start: var(--icon-trash)">. Considérez cela comme une solution de secours plutôt que comme l'option par défaut : dès que la même icône apparaît plusieurs fois sur le même type d'élément, déplacez-la dans une règle de feuille de style.

Commandes avec icône uniquement

Les boutons contenant uniquement une icône utilisent la classe .icon et l'élément --icon décrits ci-dessus. Donnez au bouton un nom accessible avec aria-label ou laissez l'icône en fournir un via --icon-style-alt. Sans l'un ou l'autre, l'icône générée par CSS est décorative. Pour les icônes informatives autonomes, exposez l'élément .icon avec role="img" et aria-label :

.button-close {
  --icon: var(--icon-close);
  --icon-style-alt: "Close";
}
<!-- Icon-only interactive control, icon and name from the stylesheet -->
<button class="icon button-close"></button>

<!-- Icon-only interactive control, named via aria-label -->
<button class="icon" style="--icon: var(--icon-trash);" aria-label="Delete item"></button>

<!-- Standalone informative icon -->
<span class="icon" style="--icon: var(--icon-favorite);" role="img" aria-label="Favorite"></span>

Utiliser directement le format SVG

Lorsque le système d'icônes CSS n'est pas adapté, utilisez le SVG intégré et <use> ensemble plutôt que comme alternatives. Insérez le SVG la première fois qu'une icône apparaît sur une page et référencez-le avec <use href="#id"> pour chaque répétition. La référence locale ne coûte aucune requête et n'est soumise à aucune restriction interorigine. Réservez un sprite externe (<use href="/icons.svg#id">) pour les icônes partagées sur de nombreuses pages.

Avec <use>, le contenu référencé est cloné dans un shadow tree, de sorte que le CSS de la page ne peut pas sélectionner ses éléments. Ce qui est hérité : color, currentColor et toute propriété personnalisée. Au-delà des attributs de présentation SVG, le CSS moderne peut également modifier de nombreux attributs géométriques. Ainsi, une icône que vous contrôlez peut exposer exactement les hooks dont vous avez besoin, par exemple stroke-width="var(--icon-stroke, 2)" ou même d: var(--icon-path, path(…)) sur un tracé particulier.

La propriété CSS d n'est pas compatible avec Safari. N'utilisez donc cette personnalisation que lorsque cette absence de compatibilité est acceptable. Si vous ne contrôlez pas l'icône, intégrez-la.

<use> points de vigilance à connaître :

  • viewBox doit se trouver sur <symbol> ou sur <svg> référencé. L'élément <svg> externe encapsulant <use> ne le récupère pas à partir de la référence. Définissez donc width et height sur le wrapper et conservez viewBox avec le symbole.
  • Les styles ne franchissent pas la limite dans un sens ou dans l'autre : <style> à l'intérieur d'un sprite externe ne s'applique pas à la page, et les feuilles de style de la page ne s'appliquent pas à l'intérieur de l'arborescence référencée. Seules les propriétés héritées et les propriétés personnalisées sont transmises.
  • Les références externes sont de même origine uniquement. Les sprites interorigines échouent de manière silencieuse.
  • Les sprites intégrés au document partagent l'espace de noms id de la page. Par conséquent, les ID de symbole de l'espace de noms permettent d'éviter les collisions.

Marquez les SVG décoratifs avec aria-hidden="true" et donnez au contrôle conteneur son propre nom accessible, comme avec le système d'icônes CSS.

Stratégies de remplacement

Propriétés personnalisées enregistrées

Sans @property, --icon-start et --icon-end se comportent comme des propriétés personnalisées héritées ordinaires. Par conséquent, une icône définie sur un élément s'affiche également sur ses descendants. Pour éviter cela, réinitialisez les propriétés des enfants dans la requête de style (@container style(--icon-start) { * { --icon-start: initial; } }), comme dans la solution de contournement Safari ci-dessous.

Requêtes de style de conteneur

Pour prendre en charge les navigateurs sans requêtes de style de conteneur, utilisez des classes ou des sélecteurs de composants comme hooks de remplacement. Conservez la sélection d'icônes dans les propriétés personnalisées. Les sélecteurs de style intégrés seuls ne détectent pas les icônes attribuées via des règles CSS.

:where(.icon-start, .icon:empty)::before {
  content: image-set(linear-gradient(transparent 0 100%), var(--icon-start)) / var(--icon-style-alt, "");
  /* icon styles */
}

:where(.icon-start)::before {
  --icon-start: inherit;
}

:where(.icon:empty)::before {
  --icon-start: var(--icon);
}

:where(.icon-start:not(.icon, :empty))::before {
  margin-inline-end: var(--icon-style-spacing, 0.3em);
}

Pour les icônes de fin, utilisez le même hook de classe de remplacement avec .icon-end, --icon-end et ::after au lieu de .icon-start, --icon-start et ::before. Appliquez un espacement au contenu non vide avec margin-inline-start au lieu de margin-inline-end.

Ce modèle image-set() rend content non valide lorsque la propriété personnalisée n'est pas définie. Il revient donc à none et le pseudo-élément disparaît sans ajouter ni supprimer de classe. Il ne supprime pas les autres styles du pseudo-élément et n'empêche pas les conflits avec d'autres règles ::before ou ::after. Par conséquent, n'utilisez ces hooks que lorsque ces pseudo-éléments sont disponibles pour les icônes.

Solutions de contournement pour le navigateur Safari

Safari présente deux bugs de rendu qui affectent les icônes : WebKit #301609 peut empêcher leur affichage jusqu'à ce que l'utilisateur interagisse, et #320220 affecte également leur rendu. La solution de contournement ci-dessous permet de résoudre ces deux problèmes :

@keyframes webkit-301609 {}

@supports ((-webkit-nbsp-mode: normal) or (-webkit-touch-callout: none) or (font: -apple-system-body)) and (content-visibility: auto) {
  @layer webkit-301609-fix {
    ::before, ::after {
      animation: webkit-301609 0s;
    }
  }
}

La requête @supports ne cible que Safari, et @layer réduit les conflits d'animation.

Le bug WebKit n° 325900 peut empêcher l'application d'un remplacement direct de --icon-start dans certains cas, par exemple lors du changement d'icône au survol. Cela affecte la solution de contournement pour les propriétés enregistrées sans héritage. Pour Safari, enregistrez --icon-start en tant qu'héritage et réinitialisez-le sur les descendants dans la requête de style :

@supports ((-webkit-nbsp-mode: normal) or
           (-webkit-touch-callout: none) or
           (font: -apple-system-body)) and (content-visibility: auto) {
  /* Override previous declaration */
  @property --icon-start {
    syntax: "*";
    inherits: true;
  }

  @layer webkit-325900-fix {
    @container style(--icon-start) {
      * {
        --icon-start: initial;
      }
    }
  }
}

La réinitialisation empêche la propagation de la valeur de l'icône aux descendants. En le conservant dans une couche, les déclarations d'icônes sans couche ont priorité sur la réinitialisation.