Icônes

icon() renvoie un <svg> en ligne. Chaque glyphe est dessiné sur la même grille 24×24, en traits non remplis en currentColor : il hérite donc de la couleur et de la taille de texte de son contexte et n’a besoin d’aucun style propre.

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('check'),
  icon('search'),
  icon('trash'),
  icon('settings'),
)

Dans un composant

Une icône est un enfant comme un autre. Comme elle se dimensionne en em, elle s’accorde au libellé voisin sans qu’on lui dise quelle taille il fait :

stack({ direction: 'row', gap: 'sm', align: 'center', wrap: true },
  button({ color: 'primary' }, icon('download'), 'Télécharger'),
  button({ variant: 'outline' }, icon('external-link'), 'Ouvrir'),
  button({ size: 'sm', variant: 'soft', color: 'danger' }, icon('trash'), 'Supprimer'),
  iconButton({ label: 'Rechercher', variant: 'soft', icon: icon('search') }),
)

Taille

Le défaut est 1em — la taille du texte environnant. size prend un jeton ou n’importe quelle longueur CSS quand vous voulez vous en écarter :

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('star', { size: 'sm' }),
  icon('star'),
  icon('star', { size: 'lg' }),
  icon('star', { size: '2rem' }),
  icon('star', { size: '3rem' }),
)

Couleur

Il n’y a pas de prop de couleur. Une icône est dessinée en currentColor : elle prend la couleur de son contexte — c’est ce qui permet à un seul jeu de fonctionner dans cinq palettes :

stack({ direction: 'row', gap: 'md', align: 'center' },
  text({ style: 'color: var(--su-primary)' }, icon('heart', { size: 'lg' })),
  text({ style: 'color: var(--su-success)' }, icon('check-circle', { size: 'lg' })),
  text({ style: 'color: var(--su-warning)' }, icon('alert-triangle', { size: 'lg' })),
  text({ style: 'color: var(--su-danger)' }, icon('x-circle', { size: 'lg' })),
  text({ tone: 'muted' }, icon('info', { size: 'lg' })),
)

Noms accessibles

Une icône est aria-hidden par défaut, ce qui est juste bien plus souvent qu’autrement : une icône à côté du mot « Supprimer » ne doit pas être annoncée une deuxième fois. Ne lui donnez un label que lorsqu’elle porte tout le sens ; elle devient alors role="img" avec ce nom.

icon('trash')                         // décorative — masquée
button(icon('trash'), 'Supprimer')    // c’est le mot qui parle

icon('trash', { label: 'Supprimer' }) // annoncée comme une image

// Un bouton-icône nomme le bouton, pas le glyphe qu’il contient
iconButton({ label: 'Supprimer', icon: icon('trash') })

Rempli

filled peint un glyphe au lieu de le tracer en contour. C’est le même tracé dans les deux cas — seul l’attribut fill change —, si bien que les deux formes partagent exactement le même bord extérieur et ne peuvent pas diverger.

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('bell', { size: 'lg' }),
  icon('bookmark', { size: 'lg' }),
  icon('folder', { size: 'lg' }),
  icon('heart', { size: 'lg' }),
  icon('star', { size: 'lg' }),
)

Et les mêmes noms, remplis :

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('bell', { filled: true, size: 'lg' }),
  icon('bookmark', { filled: true, size: 'lg' }),
  icon('folder', { filled: true, size: 'lg' }),
  icon('heart', { filled: true, size: 'lg' }),
  icon('star', { filled: true, size: 'lg' }),
)

C’est une prop plutôt qu’un second jeu de noms parce que l’état rempli est presque toujours un état — enregistré, aimé, noté — et réclame donc un booléen, pas une chaîne différente :

icon('heart', { filled: liked })
icon('bookmark', { filled: saved, label: saved ? 'Enregistré' : 'Enregistrer' })

// plutôt que
icon(liked ? 'heart-filled' : 'heart')

Les glyphes de statut se remplissent autrement, parce que leur marque se trouve à l’intérieur de la forme. Peindre le cercle avalerait la coche : la marque y est donc découpée à la place :

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('check-circle', { filled: true, size: 'lg' }),
  icon('x-circle', { filled: true, size: 'lg' }),
  icon('info', { filled: true, size: 'lg' }),
  icon('help', { filled: true, size: 'lg' }),
  icon('alert-triangle', { filled: true, size: 'lg' }),
)

Ceux-là portent un second dessin — la forme pleine avec la marque découpée par fill-rule: evenodd — parce qu’un tel évidement ne s’obtient pas du tracé de contour en changeant un attribut. La forme extérieure est dessinée au bord extérieur du contour, donc les deux versions se terminent sur la même silhouette. C’est la même prop dans les deux cas ; le mécanisme qu’un glyphe emploie ne regarde que lui.

Un chevron n’a aucun intérieur à peindre — c’est une ligne ouverte — donc il se remplit jusqu’au triangle que décrivent ses trois points, en gardant le trait qui arrondit les angles :

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('chevron-up', { filled: true, size: 'lg' }),
  icon('chevron-down', { filled: true, size: 'lg' }),
  icon('chevron-left', { filled: true, size: 'lg' }),
  icon('chevron-right', { filled: true, size: 'lg' }),
)

fillableIcons() liste tout ce qui répond à filled. Un glyphe sans forme pleine l’ignore et reste en contour — remplir eye perdrait la pupille et tag son trou, donc ni l’un ni l’autre ne fait semblant.

Rotation

spin fait tourner le glyphe — prévu pour spinner, même si rien ne vous empêche de faire tourner refresh pendant qu’une chose se recharge. Sous prefers-reduced-motion il ralentit à peine plus qu’un souffle au lieu de s’arrêter, parce qu’un spinner arrêté a l’air cassé.

stack({ direction: 'row', gap: 'md', align: 'center' },
  icon('spinner', { spin: true, size: 'lg' }),
  icon('refresh', { spin: true, size: 'lg' }),
  button({ variant: 'soft' }, icon('spinner', { spin: true }), 'Enregistrement…'),
)

Le jeu

Les noms décrivent le dessin plutôt que le rôle qu’il joue — x-circle, pas error — parce que le même dessin sert à des rôles sans rapport, et qu’un nom décrivant l’image reste vrai quand cela arrive. Les alias ci-dessous couvrent les intentions courantes.

alert-triangle
arrow-down
arrow-left
arrow-right
arrow-up
banknote
bell
bold
bookmark
calendar
camera
check
check-circle
chevron-down
chevron-left
chevron-right
chevron-up
chevrons-left
chevrons-right
clock
close
code
coins
comment-bubble
copy
credit-card
database
download
edit
external-link
eye
eye-off
facebook
feather
file
filter
folder
gear
gift
globe
google
heart
help
home
image
info
instagram
italic
key
layers
link
linkedin
location
lock
log-in
log-out
mail
menu
minus
moon
more-horizontal
more-vertical
package
percent
pin
plus
print
receipt
refresh
search
settings
share
shopping-bag
shopping-cart
smartphone
sparkles
spinner
star
store
sun
tag
terminal
thumbs-down
thumbs-up
tiktok
trash
truck
universal-access
unlock
upload
user
users
video
wallet
whatsapp
x-circle
x-twitter
youtube
zap

Marques

Huit logos de marque viennent avec le jeu — facebook, google, instagram, linkedin, tiktok, whatsapp, x-twitter et youtube. Ils acceptent toujours size et label et se dessinent toujours en currentColor :

stack({ direction: 'row', gap: 'md', align: 'center', wrap: true },
  icon('facebook', { size: 'lg' }),
  icon('instagram', { size: 'lg' }),
  icon('x-twitter', { size: 'lg' }),
  icon('youtube', { size: 'lg' }),
  icon('whatsapp', { size: 'lg' }),
  button({ variant: 'soft', color: 'neutral' }, icon('linkedin'), 'Partager'),
)

Ce sont des reproductions de marques appartenant à d’autres, pas des dessins au style de cette bibliothèque : ils enfreignent donc deux de ses règles à dessein — ce sont des formes pleines plutôt que des traits, ce qu’est un logo, et leurs proportions sont celles de la marque, pas celles de cette grille. filled ne leur dit rien — ils le sont déjà.

Le dessin vient de Simple Icons, qui le publie sous CC0. Cela couvre le dessin, pas la marque déposée : servez-vous-en pour désigner la chose qu’ils nomment — un lien de profil, un bouton de partage — et pas sur un produit à vous.

C’est x-twitter, pas x, parce que x est déjà un alias de close et qu’un bouton de fermeture se transformant en logo serait une vilaine surprise. twitter y mène aussi.

Alias

Chacun de ceux-ci rend un glyphe listé plus haut, sous le nom auquel vous penserez sans doute en premier :

success → check-circle

warning → alert-triangle

danger, error → x-circle

x, cross → close

question → help

loading → spinner

cog, gears → gear

delete, trash-can → trash

pencil → edit

notification → bell

dots → more-horizontal

bolt, lightning → zap

arrow-back → arrow-left

arrow-forward → arrow-right

cart → shopping-cart

bag → shopping-bag

card → credit-card

cash, money → banknote

delivery, shipping → truck

shop → store

discount, sale → percent

login, sign-in → log-in

logout, sign-out → log-out

map-pin, marker → location

mobile → smartphone

like → thumbs-up

dislike → thumbs-down

comment, message, chat → comment-bubble

ai, magic → sparkles

printer → print

accessibility, a11y → universal-access

twitter → x-twitter

Vos propres icônes

registerIcons() ajoute un glyphe, ou en remplace un fourni. Le balisage est le contenu du <svg> — des formes sur la même grille 24×24, laissées sans remplissage pour que currentColor les atteigne. Appelez-le une fois depuis un module que vos pages importent :

import { registerIcons } from 'sitelo/ui'

registerIcons({
  logo: '<path d="M4 20 12 4l8 16z"/>',
  // Un nom qui existe déjà le remplace partout : c’est ainsi qu’on restyle
  // un glyphe fourni sans forker la bibliothèque.
  check: '<path d="m5 13 4 4 10-11"/>',
  // Une seule forme fermée, pour qu’il réponde à `filled` comme les glyphes fournis.
  pin: { markup: '<path d="M12 21s7-6.3 7-11a7 7 0 1 0-14 0c0 4.7 7 11 7 11z"/>', fillable: true },
})
import { icon } from 'sitelo/ui'

icon('logo')                   // votre glyphe
icon('check')                  // désormais le vôtre aussi

registerIcons({ check: null }) // et retour à celui fourni

Pourquoi en ligne, et pas un sprite

Les icônes sont rendues dans la page plutôt que tirées d’un icons.svg avec <use>. Un sprite économise de l’ordre de cent octets gzippés de HTML par page et coûte un aller-retour pour cela — le balisage répété est justement le cas où gzip excelle, donc l’essentiel de ce qu’un sprite existe pour dédupliquer l’est déjà. En ligne, il n’y a aussi aucun fichier à émettre, aucun chemin de base à configurer, et rien qui puisse manquer dans dist — le même marché que fait styles({ inline: true }).

Props

PropTypeDéfautDescription
namestring—Quel glyphe. Peut être passé comme premier argument à la place.
size'sm' | 'md' | 'lg' | string'md'Un jeton, ou n’importe quelle longueur CSS. Par défaut 1em.
labelstring—L’annoncer comme une image portant ce nom, au lieu de la masquer.
spinbooleanfalseLa faire tourner en continu.
filledbooleanfalsePeindre le glyphe au lieu de le tracer. Ignoré par les glyphes non remplissables.

Un nom inconnu ne rend rien du tout plutôt que de lever une erreur — une prop cosmétique ne devrait pas pouvoir faire échouer un build. hasIcon(name) vous dit si un glyphe existe, iconNames() les liste tous, et fillableIcons() donne ceux qui acceptent filled.