Mouvement et finition
Le problème
Les composants paraissaient plats, et ce n'était pas une impression. Le dépôt n'avait aucun token de mouvement : les durées étaient écrites à la main, .15s ici, .12s là, sans que rien ne relie les deux. Aucune occurrence de prefers-reduced-motion.
Le bouton résumait le problème. Il déclarait une transition sur trois propriétés, puis n'écrivait aucune règle :hover ni :active : il n'y avait rien à animer. La transition était déclarée dans le vide.
Les tokens
@sia-ui/tokens expose trois familles de plus.
| Token | Valeur | Rôle |
|---|---|---|
duration.instant | 80 ms | retour tactile — :active, bascule d'une case |
duration.fast | 120 ms | survol, focus, changement de couleur |
duration.base | 180 ms | ouverture d'un popover, d'un accordéon |
duration.slow | 260 ms | modale, drawer |
easing.standard | cubic-bezier(.2,.7,.3,1) | défaut, décélération marquée |
easing.entrance | cubic-bezier(0,.55,.45,1) | apparition d'une couche |
easing.exit | cubic-bezier(.4,0,1,1) | disparition, plus rapide que l'entrée |
easing.spring | cubic-bezier(.34,1.56,.64,1) | rebond léger — switch, note |
elevation.rest / hover / press | trois ombres | états d'un contrôle |
elevation.overlay / modal | deux ombres | couches flottantes |
Un composant ne choisit plus une durée, il choisit un rôle.
Une seule source pour le thème
Le bloc :root de la feuille de style était recopié à la main : changer une couleur demandait de la modifier à deux endroits, et rien ne signalait quand les deux divergeaient.
Il est désormais généré depuis @sia-ui/tokens :
pnpm --filter @sia-ui/react-web build:theme # réécrit le bloc
pnpm --filter @sia-ui/react-web build:theme -- --check # échoue s'il a dérivéLe --check tourne dans build, pas dans typecheck : le script lit les tokens compilés, et seul build attend que ses dépendances le soient. Faire attendre typecheck coûterait un build complet pour vérifier des types.
Le thème sombre arrive avec : il suit prefers-color-scheme, sauf choix explicite via data-sia-color-mode.
Deux pièges rencontrés en posant ce mécanisme, l'un et l'autre invisibles en local :
- le script lisait
@sia-ui/tokensavant que le paquet ne soit construit. Chez moi undisttraînait d'une exécution précédente; en CI, non; - il écrivait en
LFdans un fichier enCRLF, donc--checksignalait une dérive à chaque aller-retour Windows/Linux. Le script respecte désormais la convention du fichier, et un.gitattributesnormalise le dépôt enLF.
La couche de mouvement
src/styles/shared.css porte ce que les composants réutilisent :
sia-fade-in,sia-scale-in,sia-slide-up— les entrées de couches;.sia-transition-colors,.sia-transition-lift— les transitions courantes;- un anneau de focus unique, sur
:where(button, a, input, …):focus-visible; - le repli
prefers-reduced-motion, qui ramène tout à0.01ms.
Ce dernier n'est pas un confort : c'est une demande médicale pour les personnes sujettes aux vertiges vestibulaires. Il ne souffre aucune exception.
Les icônes
Cinq caractères servaient d'icônes dans sept composants :
| Caractère | Où | Remplacé par |
|---|---|---|
× U+00D7 | alert, modal, multi-select, search-input, select, tags-input | XIcon |
⌄ U+2304 | select — le chevron | ChevronDownIcon |
✓ U+2713 | select — l'option choisie | CheckIcon |
⌕ U+2315 | select, search-input | SearchIcon |
★ U+2605 | rating | StarIcon |
Un glyphe hérite de la police du système : il ne se dimensionne pas en em, ne prend pas d'épaisseur de trait, ne tourne pas autour d'un centre prévisible, et ⌕ s'affiche en carré vide sur beaucoup de machines. Le chevron du Select avait d'ailleurs déjà été rafistolé deux fois en CSS pour compenser son décalage de ligne de base.
Icons compte désormais 20 tracés. Le chevron du Select tourne de 180° à l'ouverture — impossible avec un caractère, puisque son centre de rotation dépendait de la fonte.
Rating gagne au passage la demi-étoile : deux tracés superposés, celui du dessus rogné par clip-path à la part remplie. Structurellement impossible avec ★.
Ce qui a bougé, composant par composant
| Composant | Ce qui change |
|---|---|
Button, IconButton | prop tone, dégradé d'un degré, élévation de 1 px au survol, enfoncement à l'appui |
Select | chevron SVG qui pivote, panneau en scale + fade |
Modal, Popover | entrée animée, ombre de couche |
Drawer | durées passées en tokens, sortie sur easing.exit |
Switch, Checkbox | easing.spring sur le pouce et la coche, écrasement à l'appui |
Tabs | indicateur qui glisse sous l'onglet actif |
Rating | demi-étoile, agrandissement au survol |
DataTable | lignes réactives au survol et à la sélection |
Le dosage de l'ombre
Premier jet trop appuyé : le bouton plein empilait elevation.hover — qui compte déjà deux couches — et un halo coloré de 18 px. Quatre ombres pour un survol.
Deux corrections :
elevation.hoverest passée de0 2px 4px /.08, 0 8px 18px /.10à0 1px 2px /.06, 0 3px 8px /.07;- le bouton plein ne porte plus qu'une seule ombre, teintée par son ton. Sur une surface colorée, une ombre neutre empilée sur une ombre colorée ne fait qu'épaissir la tache sans rien ajouter.
Le survol garde son élévation d'un pixel; c'est le déplacement qui signale la réaction, pas la taille de l'ombre.
Cette retouche a révélé un défaut sans rapport : cinq composants — Calendar, DatePicker, DateRangePicker, TimePicker, TimeRangePicker — importaient @sia-ui/react-web, c'est-à-dire leur propre paquet : leur ./icons avait été redirigé vers le paquet avant qu'ils n'y entrent eux-mêmes. Ils pointent désormais sur ../Icons, et les dépendances du registre sont régénérées depuis les imports réels plutôt que tenues à la main.
La prop tone
.sia-button--danger existait dans la feuille de style sans qu'aucune prop ne permette de l'atteindre : une action de suppression ne pouvait pas être rouge sans un className manuel.
<Button tone="danger">Supprimer</Button>
<Button tone="success" variant="outline">Valider</Button>Chaque ton ne fixe qu'une variable, --sia-button-accent; les trois variantes s'y réfèrent. Ajouter un ton, c'est une ligne.
Voir le résultat
pnpm storybookLa story Fondations / Motion montre les tons, le ressort, les durées à l'échelle et les élévations.