Skip to content

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.

TokenValeurRôle
duration.instant80 msretour tactile — :active, bascule d'une case
duration.fast120 mssurvol, focus, changement de couleur
duration.base180 msouverture d'un popover, d'un accordéon
duration.slow260 msmodale, drawer
easing.standardcubic-bezier(.2,.7,.3,1)défaut, décélération marquée
easing.entrancecubic-bezier(0,.55,.45,1)apparition d'une couche
easing.exitcubic-bezier(.4,0,1,1)disparition, plus rapide que l'entrée
easing.springcubic-bezier(.34,1.56,.64,1)rebond léger — switch, note
elevation.rest / hover / presstrois ombresétats d'un contrôle
elevation.overlay / modaldeux ombrescouches 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 :

bash
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/tokens avant que le paquet ne soit construit. Chez moi un dist traînait d'une exécution précédente; en CI, non;
  • il écrivait en LF dans un fichier en CRLF, donc --check signalait une dérive à chaque aller-retour Windows/Linux. Le script respecte désormais la convention du fichier, et un .gitattributes normalise le dépôt en LF.

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èreOùRemplacé par
× U+00D7alert, modal, multi-select, search-input, select, tags-inputXIcon
⌄ U+2304select — le chevronChevronDownIcon
✓ U+2713select — l'option choisieCheckIcon
⌕ U+2315select, search-inputSearchIcon
★ U+2605ratingStarIcon

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 ​

ComposantCe qui change
Button, IconButtonprop tone, dégradé d'un degré, élévation de 1 px au survol, enfoncement à l'appui
Selectchevron SVG qui pivote, panneau en scale + fade
Modal, Popoverentrée animée, ombre de couche
Drawerdurées passées en tokens, sortie sur easing.exit
Switch, Checkboxeasing.spring sur le pouce et la coche, écrasement à l'appui
Tabsindicateur qui glisse sous l'onglet actif
Ratingdemi-étoile, agrandissement au survol
DataTablelignes 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.hover est passée de 0 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.

tsx
<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 ​

bash
pnpm storybook

La story Fondations / Motion montre les tons, le ressort, les durées à l'échelle et les élévations.

Publié sous licence MIT.