Tous les champs — texte, sélection, case à cocher — partagent un seul libellé. Pas une copie conforme dans chacun : le même fichier, appelé par tous. Ce qui change ici a déjà changé partout.
FieldLabel
Ce qu'il est. Le mot qui nomme un champ, et le signe qui dit s'il faut le remplir. Ce qu'il n'est pas. Ni un titre — il ne structure pas la page, il désigne une chose à remplir — ni le porteur de l'obligation : c'est le champ qui porte required, et c'est lui que les lecteurs d'écran annoncent. L'astérisque n'en est que la trace visible.
default
default + required
disabled
disabled + required
L'astérisque est redondant, et c'est voulu. Il est aria-hidden : sans quoi il serait annoncé deux fois, une fois en « étoile », une fois en « obligatoire ». En contrepartie, le formulaire qui s'en sert doit ouvrir sur la phrase « Les champs marqués * sont obligatoires ». Son rouge est celui du système, et il ne l'a pas toujours pu : color/feedback/danger valait #D94A4A, soit 4,18:1 sur blanc, et ne tenait pas le seuil du texte de corps — on l'admettait parce que le signe est redondant. Le cran s'appelle maintenant color/feedback/danger/500 et vaut #DC2626, soit 4,82:1 : la dérogation n'a plus lieu d'être.
Props
Prop
Type
Défaut
Ce qu'elle fait
for
string
—
L’id du champ que ce libellé nomme. C’est ce qui les lie. Sans objet en légende.
as
'label' | 'legend'
'label'
legend quand le libellé nomme un groupe, en premier enfant d’un fieldset.
required
boolean
false
Pose l’astérisque. Le champ doit porter required de son côté.
disabled
boolean
false
L’encre passe en muted. Le champ doit porter disabled de son côté.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Text field
Ce qu'il est. Une ligne où quelqu'un tape du texte — un nom, une adresse, un sujet. Ce que le système ne peut pas proposer d'avance. Nœud Figma 23:29, six variantes. Ce qu'il n'est pas. Ni Text Area, qui est la même chose sur plusieurs lignes et qui compte ses caractères ; ni Select, dont la liste est close ; ni un champ à suggestions — la liste qui s'ouvrirait dessous n'existe nulle part dans le système.
Le système a cru pendant tout un sprint qu'il n'avait pas de champ texte.Le sprint du 27/08 a relevé les neuf composants de formulaire de la plage 340–355 ; celui-ci est en 23, dessiné bien avant les autres, et il est resté invisible — ni dans les intégrés, ni dans la liste des non-intégrés. Sa description disait pourtant l'essentiel : il partage sa hauteur avec le bouton, pour que les deux se posent au même niveau côte à côte — 36 pxalors, 44 depuis que control/height la porte.
Son libellé vient de FieldLabel, comme celui de tous les autres champs. type ne prend que les types d'une seule ligne : un champ qui accepterait textarea ne serait plus celui-ci.
default — sur son invite
required
rempli — type="email"
disabled + required
Props
Prop
Type
Défaut
Ce qu'elle fait
id
string
—
Lie le libellé au champ, et sert de name si aucun n’est donné.
label
string
—
Le texte du libellé. Rendu par FieldLabel, jamais réécrit ici.
type
'text' | 'email' | 'tel' | 'url' | 'search'
'text'
Ce que le champ accepte. Les types d’une seule ligne, et eux seuls.
value
string
—
Le texte déjà saisi.
placeholder
string
—
Un exemple, pas une consigne : il disparaît à la première frappe.
name
string
id
Le nom envoyé par le formulaire.
maxlength
number
—
La limite de caractères. Ici elle ne compte rien : c’est Text Area qui a le compteur.
autocomplete
string
—
Ce que le navigateur a le droit de proposer. Un nom, une adresse, un code.
required
boolean
false
Pose l’astérisque sur le libellé et l’attribut sur le champ.
disabled
boolean
false
Grisé des deux côtés — libellé et champ.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Le rembourrage du nœud valait 6 / 14, que rien ne liait. 14 n'existe dans le système que comme rayon, et 6 n'existe pas du tout. Le nœud a été redessiné à 12 / 16, les deux button/padding-*/sm — le geste déjà fait sur Text Area le 27/08. Les trois champs s'espacent maintenant pareil. Contre une hauteur fixe de 44, le rembourrage vertical ne pousse rien : il dit seulement d'où le texte part.
La variante Multiline n'est pas implémentée. Elle dessine ce que Text Area fait déjà, avec son propre nœud et son compteur. Deux composants pour une même chose se sépareraient au premier changement — c'est celui qui a le compteur qui reste.
Le nœud n'a ni état désactivé ni état d'erreur, et sa description dit pourquoi : en inventer un reviendrait à inventer des couleurs. disabled est pourtant écrit, parce que les six autres champs le portent et qu'il ne tient qu'à opacity/disabled, qui existe déjà. L'erreur, elle, ne l'est pas : elle demanderait une encre, un trait et un message qui n'existent nulle part. C'est la seule chose qui manque à ce champ pour être complet, et elle se dessine dans Figma.
Les trois champs se dessinent au même endroit. Police, encre, fond, trait, rayon, rembourrage, hauteur, survol, état éteint : tout est écrit une fois dans global.css, en recette de balise — à côté de celle du h2 et du p. Text field, Text Area et Select ne déclarent plus que leur place et ce qui leur est propre : le chevron pour l'un, le compteur pour l'autre. Ce qui change là a déjà changé pour les trois, comme FieldLabel le fait du libellé.
Les trois survols n'en font plus qu'un, et c'est celui-ci. Le trait fonce en neutral/500, le fond ne bouge pas. Text Area et Select grisaient le leur en neutral/100 — un cran qui ne se voit pas : #F3F2F5 et #F3F0FF ne diffèrent que par le canal bleu, 245 contre 255. Les deux nœuds ont été redessinés sur celui-ci.
La limite de la boîte ne se voit presque pas. Le trait border/default vaut 1,28:1 sur le fond du champ, et ce fond 1,12:1 sur le blanc de la page — WCAG 1.4.11 en demande 3 pour la limite d'un contrôle. C'est exactement l'écart réglé sur le Chip le 31/08, en rebranchant son trait sur accent/300 (3,2:1) sans toucher à border/default, que d'autres composants lisent. Le même geste est possible ici, sur les trois nœuds à la fois.
Select
Un champ qui fait choisir une valeur, ou plusieurs avec multiple, dans une liste. Nœud Figma 340:933.
Choix unique
Simple
Une liste, un choix. La liste se referme dès qu’on a choisi.
Voir le code
---
import Select from 'ux-design-system/components/Select.astro';
---
<Select
id="u-simple"
label="Pays"
options={['France', 'Belgique', 'Suisse', 'Canada']}
placeholder="Choisir un pays"
/>
Avec le choix épinglé
showSelected garde le choix visible en bas de la liste, même quand elle défile.
Préfixe des id des contrôles, et name si aucun n’est donné. Unique dans la page.
label
string
—
Le texte du libellé — la légende du groupe, rendue par FieldLabel.
options
(option | { label, options, disabled? })[]
—
Les choix. Une chaîne vaut pour sa propre valeur. Un groupe range ses choix sous un nom, comme un optgroup ; disabled l’éteint en entier.
multiple
boolean
false
Plusieurs choix : des cases plutôt que des radios. MultiSelect n’est que ce composant avec multiple.
value
string
—
Le choix présélectionné, pour un choix unique.
values
string[]
[]
Les valeurs cochées à l’ouverture, pour multiple.
max
number
—
multiple — le nombre de choix à ne pas dépasser. Absent : aucune limite.
display
'count' | 'chips'
'count'
multiple — ce que montre le champ de ses choix : un compteur, ou en plus des pastilles.
visibleOptions
number
7
Le nombre de lignes que la liste montre avant de défiler.
searchable
boolean
false
Pose un champ de recherche en haut de la liste, qui filtre les options.
showSelected
boolean
false
Épingle en bas de la liste ce qui est choisi, sous un trait. Cliquer une ligne retire le choix.
creatable
boolean
false
Pose en bas de la liste la ligne qui crée une option. La page entend l’événement select-create (detail.value, detail.label) et l’enregistre.
searchLabel
string
Filtrer les options
Le nom lu du champ de recherche. Il n’est pas affiché.
searchPlaceholder
string
Rechercher
L’invite du champ de recherche.
noResultsLabel
string
Aucun résultat
Ce que dit la liste quand aucune option ne correspond.
placeholder
string
Choisir
Ce que dit la boîte quand rien n’est choisi.
countOneLabel
string
{n} sélectionné
multiple — ce que dit la boîte pour un seul choix. {n} vaut le nombre.
countLabel
string
{n} sélectionnés
multiple — ce que dit la boîte pour plusieurs choix.
maxLabel
string
Maximum atteint : {n} sur {max}
multiple — le message de limite.
removeLabel
string
Retirer
Le verbe d’une pastille, lu avant son mot : « Retirer France ».
createLabel
string
Créer une option
La ligne de création au repos.
createQueryLabel
string
Créer « {q} »
La ligne de création quand un texte est tapé dans la recherche. {q} vaut ce texte.
createPlaceholder
string
Nom de l’option
L’invite du champ où l’on tape le nom de l’option créée.
requiredMessage
string
Choisir au moins une option.
multiple — ce que le navigateur dit si required et rien n’est coché.
name
string
id
Le nom envoyé par le formulaire, le même pour tous les contrôles.
required
boolean
false
Au moins un choix. Pose l’astérisque.
disabled
boolean
false
Éteint le champ entier, et la boîte ne s’ouvre plus.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Notes de conception
Choix unique
Ce qu'il est. Un champ qui fait choisir une valeur, ou plusieurs avec multiple, dans une liste. Nœud Figma 340:933 (forms/Select), dont l'état Open accroche la liste forms/Select List Combined (778:4266). Ce qu'il n'est pas. Ni un menu — un menu déclenche des actions, celui-ci renseigne une valeur qu'un formulaire enverra — ni un champ à suggestions : la liste est close, et seule creatable y laisse entrer une option, par décision de la page.
Son libellé vient de FieldLabel, comme celui de tous les autres champs.Il n'est plus un <select> natif. Le dessin a demandé la recherche, la création d'une option et le choix épinglé en bas, y compris pour une valeur seule : une liste ouverte par le système d'exploitation est hors d'atteinte du CSS. C'est maintenant un <fieldset>, un bouton qui ouvre une liste flottante, et des radios — des cases avec multiple — qui portent le même name. Le formulaire envoie ce qui est choisi, et le clavier reste celui du navigateur : Entrée ouvre, Tab parcourt, les flèches changent de ligne. Un choix unique referme la liste dès qu'on a choisi.
Ce composant a longtemps été écrit sans son nœud. On le croyait « spécifié en conversation » — il ne l'était pas, personne ne l'avait cherché. Le relire a corrigé trois mesures que rien ne signalait : la boîte fait 36 et non les 44 du bouton, le chevron 20 et non 24, l'écart au libellé 5 et non 8. button/icon-size/sm vaut 20 et control/field-gap 5. La boîte est depuis revenue aux 44 du bouton, portés par control/height — voir plus bas.
Ce champ porte enfin un trait au repos. Sa boîte n'en avait aucun, là où Text field et Text Area en portaient un de 1 — trois champs voisins, deux dessins. Le nœud a été redessiné, et ses quatre états lisent maintenant border/default : le même trait que les deux autres, à la même épaisseur et du même côté. Le fond bg/surface, lui, n'a pas changé.
44 de haut et 12 de rembourrage vertical ne tiennent pas ensemble, et les deux champs d'une ligne le déclarent tous les deux. Le texte fait 14 à 120 %, soit 16,8 : 12 + 16,8 + 12 = 40,8, et la boîte en mesure 44. Sur un <input> ça ne se voit pas — le navigateur centre le texte et ignore le rembourrage. Sur un <select>, il coupe : les jambages de « Choisir un pays » disparaissaient. La recette met donc le rembourrage vertical à zéro pour les deux, la hauteur faisant seule le centrage. C'est ce que fait Button : sa hauteur prime, son padding-y n'est pas appliqué.
La boîte est de 44 : control/height. Elle a été dessinée à 36 — space/2xl + space/xs, qu'aucune variable ne nommait —, puis portée aux button/height/sm, la cible tactile de WCAG 2.5.5 (niveau AAA). Le champ se vise alors comme un bouton, et les deux se posent au même niveau côte à côte. La variable porte son propre nom plutôt que d'alias celui du bouton : un champ qui change de hauteur ne doit pas entraîner le bouton avec lui.
Choix multiple
Ce qu'il est. Un champ qui fait choisir plusieurs valeurs dans une liste fermée, et qui dit combien sont choisies. Ce qu'il n'est pas. Ni un groupe de Chips à plat : la liste est repliée, elle ne prend sa place qu'à la demande. Ce n'est pas non plus un autre composant : MultiSelect n'est plus que le Select avec multiple, gardé importable pour que les sites qui l'emploient ne changent rien.
Pourquoi pas un <select multiple>. Avec appearance: base-select, le navigateur laisse maintenant dessiner la boîte et chacune de ses options. Quatre raisons tiennent pourtant ce champ à l'écart : Firefox retombe sur la liste classique, où l'on choisit au Ctrl+clic ; un <select multiple>est une boîte toujours ouverte, sans état fermé qui dirait « 2 sélectionnés » ; sa coche est un pseudo-élément, pas le dessin du Checkbox ; et la limite comme les pastilles demanderaient un script de toute façon.
Ce champ est donc un groupe de cases : un <fieldset> nommé par sa légende, une liste qui s'ouvre sous la boîte, et des Checkbox qui portent toutes le même name. Le formulaire envoie toutes les valeurs cochées, et le clavier est celui du navigateur : Entrée ouvre, Tab parcourt, Espace coche. Un clic à côté ou Escape referme, et le focus revient à la boîte. Chaque option réagit en entier au survol et au focus, pas seulement sa case, et une option cochée passe son mot en semibold : l'état ne se lit pas que par la couleur (WCAG 1.4.1). La boîte montre l'anneau de focus au clic comme au clavier, comme le Select.
Les props sont celles du tableau de la section précédente : multiple y est posé d'office, et celles qui ne valent que pour lui y sont marquées.
Au maximum, les cases restantes s'éteignent, et un message le dit sous la boîte. Décocher une case les rallume. Le message est une région vivante : il est annoncé une fois, quand la limite tombe — pas à chaque case. Une pastille n'est pas un Chip. Un Chip est une case : il enverrait sa valeur une seconde fois. C'est un bouton qui décoche la case de la liste, au dessin du Chip retenu, avec une croix.
Il porte un script, chargé une seule fois pour tous les champs de la page, et il le justifie comme TextArea : sans lui, au premier clic, la boîte dit « 2 sélectionnés » quand il y en a trois, et les pastilles montrent des choix qui n'en sont plus — l'affichage serait faux. Le navigateur ne sait pas non plus borner le nombre de cases cochées, ni exiger qu'au moins une le soit. Sans le script, on choisit et on envoie encore.
Nœuds Figma. La boîte est forms/Select (340:933), dont l'état Open accroche forms/Select List Combined (778:4266) : dix variantes, List (single ou multi) × Search × Create option × Selected. Son contenu est forms/Select List (777:1349, simple ou groupée, ses noms de groupe en forms/Menu Group Label, 586:5379), faite de forms/Single Select Menu Item (777:1216) ou forms/Multi Select Menu Item (586:4868). La ligne de création est forms/Select Menu Item Create Option (777:1968), la pastille forms/Chip Remove (586:4921). Chaque mesure y est liée à une variable.
Trois compléments, chacun à sa place dans la liste.searchable pose la recherche en haut. showSelected épingle en bas ce qui est choisi, sous un trait : une ligne épinglée est une case cochée, dessinée comme celle d'une option — même pour un choix unique, c'est le dessin du nœud. La décocher retire le choix, et la zone n'existe que s'il y a un choix. creatable pose en bas la ligne « Créer une option » : elle devient un champ où l'on tape le nom et, validé par Entrée ou le plus, l'option entre dans la liste, choisie. Avec la recherche et un texte tapé, elle dit « Créer « ce texte » » et crée aussitôt ; elle disparaît si l'option existe déjà. Le champ n'enregistre pas ce qu'il crée : il l'annonce à la page par l'événement select-create, dont detail.value et detail.label sont l'option. C'est à la page de la garder.
Trois écarts venus des pièces qu'il emprunte, relevés en le dessinant : le libellé du Select est en LOWER dans Figma — « destinations » — quand le code ne touche pas à la casse ; les variantes cochées Hover, Focused et Disabled du Checkbox dessinent une coche coupée ; et l'anneau de focus d'une pastille retenue, comme celui du Chip retenu, est de la couleur de son fond — sans l'écart de button/focus-offset que le code lui donne, il ne se voit pas.
La liste est simple ou groupée, selon ce qu'on lui donne : une option peut être remplacée par un groupe, { label, options, disabled? }, qui range ses choix sous un nom — comme un <optgroup>. Le nom ne se coche pas et ne prend pas le focus ; il s'écrit au petit corps des libellés, en capitales atténuées. Les groupes ne s'imbriquent pas, et disabled éteint tout le groupe. Avec la recherche, un groupe dont rien ne correspond disparaît avec son nom.
Avec searchable, un champ de recherche ouvre la liste. Il prend le focus dès qu'elle s'ouvre et filtre à chaque lettre, sans casse ni accents, sur ce que l'option contient : « grece » trouve « Grèce », « isse » trouve « Suisse ». Le filtre ne décoche rien — une option masquée reste choisie et envoyée — et s'efface à la fermeture. Quand rien ne correspond, la liste le dit. Le champ reste en haut : ce sont les options qui défilent, pas le cadre.
La liste flotte, et rien n'y est écrit à la main. C'est un popover natif : le navigateur la pose dans sa couche réservée, au-dessus de toute la page, sans ordre d'empilement à inventer. L'ancrage CSS l'accroche sous la boîte et lui en donne la largeur ; elle passe au-dessus quand la place manque en bas. Sa hauteur montre visibleOptions lignes — sept par défaut — puis défile : une ligne mesure control/size plus deux space/sm, et la hauteur suit ces variables. Chaque ligne est celle du Menu Item avec une case devant : les listes à un et à plusieurs choix ont le même rythme. Elle reste à sa place dans le document, donc dans le formulaire : ses cases sont envoyées avec lui.
Radio
Ce qu'il est. Un choix parmi plusieurs, dont un seul peut être retenu. Nœud Figma 340:879, huit variantes — Selected × State. Ce qu'il n'est pas. Ni une case à cocher — une case se coche et se décoche toute seule, un radio ne se relâche qu'en en choisissant un autre — ni un objet qui vit seul : il n'a de sens que dans un groupe qui partage un name et pose sa question en <legend>. Pour un oui/non isolé, c'est Checkbox.
Le contrôle est natif, le dessin est du système.appearance: none efface celui du navigateur, et tout ce qui le remplace est une variable. Le point du centre n'est pas un second élément : c'est une ombre intérieure de space/xs posée sur un fond blanc — 16 − 4 − 4 = 8, exactement le point du nœud, sans qu'une mesure soit écrite à la main. Le contrôle reste un <input type="radio"> : la navigation aux flèches dans le groupe et l'annonce aux lecteurs d'écran viennent avec.
Un groupe, tel qu'il s'écrit
Un radio ne se juge pas seul. Le <fieldset> et sa <legend> ne viennent pas du composant : c'est la page qui pose la question, le composant ne connaît que ses propres mots.
Les états, un par un
default
selected
disabled
disabled + selected
Hover fonce l'accent d'un cran — accent/600 au lieu de accent/500 — et ne s'attrape qu'à la souris. Focused pose l'anneau du système — button/focus-ring-width sur color/border/focus, en outline pour ne rien déplacer autour de lui. WCAG 2.4.7.
Props
Prop
Type
Défaut
Ce qu'elle fait
id
string
—
Lie le libellé au contrôle. Unique dans la page, pas seulement dans le groupe.
label
string
—
Le mot qui nomme ce choix-ci — pas la question, qui va en legend.
name
string
—
Le name du groupe. Deux radios ne s’excluent que s’ils le partagent.
value
string
id
Ce que le formulaire enverra si ce choix est retenu.
checked
boolean
false
Le choix retenu à l’ouverture. Un seul du groupe doit le porter.
disabled
boolean
false
Grisé des deux côtés — contrôle et libellé.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Pas de required, et c'est voulu. Un astérisque par option dirait que chaque choix est obligatoire. C'est le groupe qui l'est : l'obligation se pose sur le <fieldset>, dans la question, pas sur chacune de ses réponses.
Plus une seule mesure approchée. Le cercle fait control/size, son trait border/width/default, son anneau de focus border/width/focus. Et le point ne se mesure toujours pas : le trait prend 1 de chaque côté, la boîte intérieure retombe à 16, et l'anneau de space/xs n'y laisse que 16 − 4 − 4 = 8. Le désactivé, lui, ne bascule plus vers un gris — le cercle garde son dessin et s'atténue de opacity/disabled, ce que le nœud dessinait depuis le début.
Le libellé n'a pas la typographie du nœud. Figma le dessine à 14 px, interligne 120 %, et le grise en neutral/400 ; FieldLabel le rend à 15 px, interligne 112 %, grisé en muted. Le libellé partagé l'emporte : c'est sa raison d'être qu'un seul fichier décide de tous les libellés du formulaire. L'écart est à trancher dans Figma, pas ici.
Checkbox
Ce qu'elle est. Une case qui se coche et se décoche toute seule. Nœud Figma 340:842, huit variantes — Checked × State. Ce qu'elle n'est pas. Ni un radio — un radio ne se relâche qu'en en choisissant un autre, et n'a de sens qu'en groupe — ni un interrupteur : un Toggle applique son effet à l'instant où on le bascule, une case attend l'envoi du formulaire.
La coche n'est pas dessinée en CSS. C'est l'icône check du registre, posée par-dessus la case et rendue insensible au clic — exactement comme Select pose son chevron sur sa boîte. Le dessin du système sert donc à deux endroits au lieu d'être refait ici.
Elle vit très bien seule, et c'est là que required a un sens : « j'accepte les conditions » est une case unique et obligatoire. Un radio, lui, n'a jamais d'astérisque — l'obligation y porte sur le groupe.
Une liste, telle qu'elle s'écrit
Les états, un par un
default
checked
default + required
disabled
disabled + checked
Hover fonce l'accent d'un cran — accent/600 au lieu de accent/500. Focused pose l'anneau du système — button/focus-ring-width sur color/border/focus, en outline pour ne rien déplacer autour de lui. WCAG 2.4.7.
Props
Prop
Type
Défaut
Ce qu'elle fait
id
string
—
Lie le libellé au contrôle. Unique dans la page.
label
string
—
Ce que la case affirme quand elle est cochée. Une phrase, pas une question.
name
string
id
Le nom envoyé par le formulaire.
value
string
on
Ce que le formulaire enverra si la case est cochée.
checked
boolean
false
Cochée à l’ouverture.
required
boolean
false
Pose l’astérisque et l’attribut. A du sens ici : une case peut être seule.
disabled
boolean
false
Grisé des deux côtés — contrôle et libellé.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Plus une seule mesure approchée. La boîte fait control/size, son rayon radius/sm, son trait border/width/default. Le trait est posé cochée comme décochée, et c'est sa couleur qui change : le nœud ne le dessine que sur la case vide, mais un trait accent sur un fond accent se voit pareil — et la boîte garde alors la même taille dans les deux états, ce qui compte davantage.
L'épaisseur du tracé de la coche est celle de l'icône du registre, pas les 2 du nœud. C'est voulu : le même dessin sert partout ailleurs dans le système, et il ne se redessine pas pour une case.
Le libellé n'a pas la typographie du nœud — 14 px / 120 % et neutral/400 dans Figma, 15 px / 112 % et muted dans FieldLabel. Même écart que pour Radio, même arbitrage : le libellé partagé l'emporte, et l'écart se tranche dans Figma.
Toggle
Ce qu'il est. Un interrupteur : deux positions, et l'effet suit la bascule. Nœud Figma 340:912, huit variantes — Value × State. Ce qu'il n'est pas. Ni une case à cocher — une case enregistre une intention que l'envoi du formulaire ira chercher, un interrupteur agit tout de suite, et c'est pourquoi il ne s'accompagne jamais d'un bouton « Valider » — ni un bouton : un bouton fait une chose et retombe, un interrupteur tient un état qu'on peut relire sur lui.
La piste tombe juste, et c'est la bonne surprise du sprint. Ses 40 de large sont space-xl + space-lg — 24 + 16, exactement. La pastille est space/lg, soit 16 pile. Son retrait dans la piste est space/xs, soit (24 − 16) / 2 = 4. Et sa course est space/lg : 40 − 16 − 4 − 4 = 16. Quatre mesures, quatre tokens, aucun chiffre écrit à la main.
Les états, un par un
off
on
disabled — off
disabled — on
Le libellé nomme un état, pas une action. « Notifications par courriel », pas « Activer les notifications » : l'interrupteur dit déjà qu'on active, et un libellé qui le répète devient faux dès qu'on l'éteint. C'est aussi la raison de role="switch" — il fait annoncer « activé / désactivé » plutôt que « coché / décoché », ce qui est la bonne phrase pour un objet qui agit à l'instant où on le bascule.
Props
Prop
Type
Défaut
Ce qu'elle fait
id
string
—
Lie le libellé au contrôle. Unique dans la page.
label
string
—
Ce que l’interrupteur allume. Un état, pas une action : « Notifications », pas « Notifier ».
name
string
id
Le nom envoyé si l’interrupteur est dans un formulaire.
checked
boolean
false
Allumé à l’ouverture.
disabled
boolean
false
Grisé des deux côtés — contrôle et libellé.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Deux mesures nommées, et tout le reste se déduit. La piste prend control/track-height et la pastille space/lg ; son retrait (22 − 16) / 2 = 3 et sa course 40 − 16 − 3 − 3 = 18 ne sont pas écrits mais calculés. Si l'une des deux bouge dans Figma, les deux autres suivent toutes seules. L'écart au libellé, lui, est passé à 8 dans le nœud : les trois contrôles compacts s'espacent enfin pareil.
Le libellé n'a pas la typographie du nœud — 14 px / 120 % et neutral/400 dans Figma, 15 px / 112 % et muted dans FieldLabel. Même écart que pour Radio et Checkbox, même arbitrage.
Text Area
Ce qu'elle est. Un champ pour du texte qui court sur plusieurs lignes. Nœud Figma 343:273, quatre états plus Show Count. Ce qu'elle n'est pas. Ni un champ texte — un champ texte tient sur une ligne et se remplit d'un nom, d'un courriel, d'un nombre, quelque chose dont on connaît la forme ; une zone de texte attend une phrase dont on ne connaît que la longueur maximale, et c'est pourquoi elle est la seule à compter ses caractères — ni un éditeur : rien ici ne met en gras, n'insère de lien ni ne garde de mise en forme.
La hauteur se dit en lignes, pas en pixels.rows compte des lignes : la boîte se mesure alors sur sa propre typographie et grandit avec elle, alors qu'une hauteur en pixels se décrocherait au premier changement de corps. Les 120 px du nœud n'ont de toute façon pas de variable.
Les états, un par un
default — sur son invite
rempli
required
disabled
Avec son compteur
Le compteur n'a de sens qu'avec une limite : « 12 caractères » ne dit rien, « 12 / 500 » dit combien il en reste. showCount sans maxlength n'affiche donc rien.
showCount + maxlength
0 / 500
showCount + maxlength — déjà rempli
69 / 500
C'est le seul script du dépôt, et il se justifie par un échec mesurable. Sans lui, le compteur afficherait « 0 / 500 » alors que la boîte en contient quarante : il ne serait pas dégradé, il serait faux. Ce qu'il enrichit reste facultatif — la limite est tenue par maxlength, côté navigateur, et le champ fonctionne entièrement sans lui. Le compteur n'est pas une région vivante : annoncer un nouveau total à chaque frappe couvrirait la dictée du texte lui-même. C'est maxlength que les lecteurs d'écran annoncent, une fois, à l'entrée dans le champ.
Props
Prop
Type
Défaut
Ce qu'elle fait
id
string
—
Lie le libellé au champ, et sert de name si aucun n’est donné.
label
string
—
Le texte du libellé. Rendu par FieldLabel, jamais réécrit ici.
value
string
—
Le contenu déjà saisi.
placeholder
string
—
L’invite dans la boîte vide. Une consigne, pas un exemple à recopier.
rows
number
2 (natif)
Le nombre de lignes visibles. C’est la hauteur du champ, exprimée en lignes.
maxlength
number
—
La limite de caractères. C’est elle qui allume le compteur.
showCount
boolean
false
Montre le compteur. Sans maxlength, il n’y a rien à compter.
name
string
id
Le nom envoyé par le formulaire.
required
boolean
false
Pose l’astérisque sur le libellé et l’attribut sur le champ.
disabled
boolean
false
Grisé des deux côtés — libellé et champ.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
L'écart au libellé est tranché, et il vaut 5. Les deux nœuds le disaient déjà — control/field-gap le nomme, et Text Area et Select s'espacent donc pareil. Le rembourrage aussi : le nœud a été redessiné sur les 12 / 16 du bouton, que le code écrivait déjà. La boîte porte enfin son trait, et le champ désactivé s'atténue au lieu de compter sur l'encre de son libellé.
Son survol a changé, et il ne lui appartient plus. Le nœud grisait son fond en neutral/100 ; les trois champs foncent maintenant leur trait en neutral/500, le geste que seul le nœud du champ texte dessinait. 343:273 a été redessiné dessus, et son dessin vient désormais de la recette que les trois partagent.
La casse TITLE du libellé n'est pas appliquée. Elle capitaliserait chaque mot du texte que l'appelant donne — ce qui est faux dès qu'un libellé porte un nom propre, une abréviation ou un mot que le français ne capitalise pas.
Le libellé ne pâlit pas au survol ni au focus. Figma le passe en muted dans ces deux états ; FieldLabel n'en a que deux, et un libellé qui pâlit quand on entre dans son champ est un arbitrage de dessin, pas une mécanique. Il se tranche dans Figma. Même remarque pour sa typographie — 14 px / 155 % contre 15 px / 112 %.
Chip
Ce qu'il est. Une pastille qu'on retient ou qu'on relâche. Nœud Figma 351:273, huit variantes — Selected × State. Ce qu'il n'est pas. Ni une étiquette — une étiquette affiche une catégorie, on ne la touche pas — ni un bouton : un bouton déclenche et retombe, un chip reste enfoncé.
C'est une case à cocher, et c'est ce qui lui permet de fonctionner sans une ligne de JavaScript : cliquer le chip le retient, la barre d'espace aussi, et le formulaire l'envoie. La case est déportée hors du cadre plutôt que masquée — display: none la retirerait du parcours au clavier et de l'arbre d'accessibilité, ce qui reviendrait à supprimer le contrôle pour n'en garder que le dessin.
C'est le premier composant du sprint qui arrive presque entièrement tokenisé. Rembourrage space/sm et space/lg, rayon radius/pill, les quatre fonds, toute la typographie : Figma les lie tous à des variables. Rien de sa forme n'est décidé dans le code.
Une rangée de filtres
Les états, un par un
Le chip a retrouvé son trait, et il se voit. C'était l'écart le plus coûteux du sprint : son fond est bg/default, le blanc de la page, et sans bordure un chip non retenu était littéralement invisible. border/width/default sur color/accent/300 le détache.
Le trait valait color/border/default, à 1,28:1 sur le blanc — sous les 3:1 que WCAG 1.4.11 demande à la limite visible d'un contrôle. Aucune couleur de trait nommée du système n'y arrivait — border/strong vaut 1,29:1, et neutral/500 2,41:1 — donc le nœud est redessiné sur accent/300, la première couleur de la rampe à dépasser le seuil, à 3,2:1. Seuls les états non sélectionnés en dépendaient (Default, Hover, Disabled) ; Focused et les états sélectionnés sont inchangés. color/border/default lui-même n'a pas bougé — ce qui le lit ailleurs voit toujours 1,28:1.
La graisse porte l'état, pas seulement la couleur. Un chip retenu passe son mot en medium — il se reconnaît donc aussi en niveaux de gris, ce que WCAG 1.4.1 demande : la couleur ne doit jamais être le seul indice.
Props
Prop
Type
Défaut
Ce qu'elle fait
id
string
—
Lie la case au chip. Unique dans la page.
label
string
—
Le mot porté par la pastille. Court : un chip ne se coupe pas en deux lignes.
name
string
id
Le nom envoyé par le formulaire. Le même pour tout un groupe de filtres.
value
string
id
Ce que le formulaire enverra si le chip est retenu.
selected
boolean
false
Retenu à l’ouverture.
disabled
boolean
false
Grisé, et ne prend plus ni le survol ni le clic.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Ce que le chip lit dans le système
Token
Où
--space-sm / --space-lg
Rembourrage — 8 en y, 16 en x
--radius-pill
La forme de pastille
--color-bg-default
Fond au repos
--color-neutral-100
Fond au survol
--color-accent-500 / --color-accent-600
Fond retenu, et retenu survolé
--color-text-default / --color-text-on-accent
Encre, selon le fond
--color-neutral-400
Encre désactivée
--font-size-ui
0.875rem — les 14 du nœud
--font-weight-regular / --font-weight-medium
Au repos, puis retenu
L'anneau de focus est porté par le chip, pas par la case — celle-ci étant hors du cadre, son propre anneau ne se verrait pas. Il mesure border/width/focus, comme partout ailleurs. WCAG 2.4.7.
Figma ne distingue pas le choix multiple du choix unique. Celui-ci est une case : plusieurs chips peuvent être retenus en même temps. Un groupe à choix unique demanderait des radios, donc une variante que le nœud ne porte pas — elle se décide dans Figma avant de s'écrire ici.
Add Image
Ce qu'elle est. Le cycle complet d'un dépôt d'image : le champ qui l'accepte, sa progression, son échec, et l'image qu'il finit par tenir. Nœuds Figma 343:315 (les quatre états d'interaction du champ) et 418:1020 (les quatre étapes du cycle). Ce qu'elle n'est pas. Un bouton — un bouton déclenche une action, l'état add ouvre le sélecteur de fichiers du système et rend un fichier au formulaire. Ce n'est plus vrai qu'elle ne montre jamais ce qui a été déposé : l'état success montre l'image, avec de quoi la revoir ou la retirer — exactement ce que fait EditImageGallery, mais pour une image dans son emplacement plutôt que pour la liste de celles déjà validées.
C'est un <input type="file"> et son <label>, et c'est ce qui lui permet de fonctionner sans une ligne de JavaScript : cliquer la zone ouvre le sélecteur, Entrée et la barre d'espace aussi. L'indication est liée au champ par aria-describedby — elle est lue après le libellé, à l'entrée dans le contrôle, et non comme un texte flottant que rien ne rattache.
Hover passe le fond en accent/100 et le libellé en accent/600. L'icône n'a rien à déclarer : en currentColor, elle suit le libellé — c'est la règle du dépôt, et c'est ce qui évite un second fichier SVG.
Props
Prop
Type
Défaut
Ce qu'elle fait
id
string
—
Lie le libellé au champ en state="add". Unique dans la page.
label
string
—
Ce que la zone propose de faire en state="add". Un verbe.
hint
string
—
Ce qu’elle accepte, en toutes lettres. Lu après le libellé, pas laissé flottant.
accept
string
—
Les types acceptés, au format de l’attribut natif.
multiple
boolean
false
Autorise plusieurs fichiers en une fois.
name
string
id
Le nom envoyé par le formulaire.
required
boolean
false
Rend le dépôt obligatoire.
disabled
boolean
false
Grisé, et n’ouvre plus le sélecteur.
state
'add' | 'in-progress' | 'fail' | 'success'
'add'
L’étape du cycle de dépôt.
progressLabel
string
'téléchargement'
Le mot affiché en state="in-progress", avant le pourcentage.
progress
number
0
La fraction déposée, 0 à 100. N’a de sens qu’en state="in-progress".
failMessage
string
—
Le message d’échec. N’a de sens qu’en state="fail".
retry
{ label, href? }
{ label: 'Recommencer' }
Le bouton qui relance en state="fail". Sans href, un bouton inerte.
image
{ id, src, alt, href?, width?, height? }
—
L’image déjà déposée, montrée en state="success".
viewLabel
string
'voir'
Le mot du bouton qui ouvre l’image en state="success".
removeLabel
string
'supprimer'
Le mot du bouton qui la retire en state="success".
removeName
string
'remove'
Le name du bouton supprimer en state="success". Sa value est l’id de l’image.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Deux mesures tombent juste par composition. Le rembourrage vertical de 28 est space-xl + space-xs, et l'icône de 32 est space/2xl — aucune des deux n'est approchée, elles sont exactes. L'horizontal, lui, est space/xl, lié dans le nœud.
La zone ne fait pas 240 × 140. Aucune variable ne porte ces deux mesures, et c'est sans doute aussi bien : la zone se dimensionne sur son contenu et prend la largeur que l'appelant lui donne, ce qu'il faut de toute façon pour une mise en page qui s'adapte.
Le trait de la zone est passé de 1,5 à 1. Cette épaisseur-là n'existait que sur deux objets du fichier — celui-ci et Add Folder, qui sont la même zone de dépôt — quand 1 est dessiné 217 fois ailleurs. Un seul cran valait mieux que deux, et les deux nœuds ont été redessinés.
La couleur de l'icône vient d'une autre bibliothèque dans Figma. Le nœud la lie à une variable étrangère à ce système. Ici l'icône est en currentColor et prend donc l'encre du libellé — c'est la règle du dépôt, et c'est ce qui la fait virer à l'accent au survol sans qu'une seconde couleur soit écrite.
Le glisser-déposer n'est pas implémenté. Il demanderait un script, et son absence n'est pas un échec : le clic et le clavier ouvrent tous deux le sélecteur.
Démo interactive
Choisir un fichier fait vivre le cycle pour de vrai : le champ passe à in-progress, puis à fail ou success en alternance — une règle de démo assumée, il n'y a pas de vrai serveur derrière cette page. Le script qui l'anime vit ici, sur /formulaire, jamais dans le composant : sans lui le champ reste un AddImage state="add" ordinaire, entièrement fonctionnel.
Ce qu'elle est. Les images déjà déposées, chacune avec de quoi la revoir ou la retirer. Nœud Figma 355:1396. Ce qu'elle n'est pas. Ni AddImage — qui couvre le cycle d'un seul emplacement, du champ vide jusqu'à l'image tenue — ni un carrousel : un carrousel fait défiler pour montrer, celle-ci étale pour laisser modifier. Et ce n'est pas une galerie de consultation : chaque vignette porte deux actions, donc elle appartient à un formulaire.
Les deux boutons sont le Button du système, en md — « voir » en secondary, « supprimer » en alert, la seule variante d'action destructive. Ils ne sont pas redessinés ici : une vignette qui aurait ses propres boutons cesserait de suivre le système au premier changement de bouton.
« supprimer » est un submit qui porte son name et sa value : le formulaire sait donc laquelle retirer sans une ligne de JavaScript. Son nom accessible dit de quelle image il s'agit — quatre boutons « supprimer » à la suite ne se distinguent pas autrement au lecteur d'écran (WCAG 2.4.6).
Les images déjà déposées. Vide : la galerie ne rend rien.
label
string
—
Ce qui nomme la liste pour les lecteurs d’écran.
removeName
string
remove
Le name du bouton supprimer. Sa value est l’id de l’image.
viewLabel
string
voir
Le mot du bouton qui ouvre l’image.
removeLabel
string
supprimer
Le mot du bouton qui la retire.
class
string
—
Classe supplémentaire pour la mise en page de l’appelant.
Ce qu'une image doit porter
Champ
Type
Ce qu'il fait
id
string
Ce que « supprimer » enverra pour désigner cette image-là.
src
string
L’adresse de l’image. Déjà passée par withBase() par l’appelant.
alt
string
Ce que l’image montre. Vide seulement si elle est purement décorative.
href
string
Où mène « voir ». Absent : le bouton n’est pas rendu.
width / height
number
Les dimensions du fichier, pour que la place soit réservée avant le chargement.
Deux mesures tombent juste par composition. La hauteur d'une vignette est space-layout-128 + space-md — 140 exactement, la mesure du nœud. Son rembourrage vertical est space-xl + space-xs, soit 28. L'écart des vignettes et le rembourrage de la grille sont space/sm, liés dans le nœud.
La largeur d'une vignette est 192, pas 189.space-layout-128 + space-layout-64 est la composition la plus proche —écart +3. Et c'est un minimum : la grille étire les vignettes pour remplir la ligne, ce qui est de toute façon ce qu'il faut ici. La largeur maximale de 337 du nœud, elle, n'a aucune variable et n'est pas écrite.
Le voile noir à 20 % n'est pas posé. Le système n'a pas d'échelle d'opacité. Il n'était là que pour détacher les boutons de la photo, et les boutons secondary ont leur propre fond opaque : le contraste tient sans lui — WCAG 1.4.3 est porté par le bouton, pas par le voile.
La vignette ne porte aucun trait, et le nœud non plus. Ce composant en a annoncé un de 1,5 jusqu'à ce qu'une relecture du nœud ne le trouve nulle part. Ce qui y est, c'est le voile à 20 %, posé sur le remplissage de l'image. La vignette, elle, n'a pas de bord à dessiner : l'image l'occupe entièrement.