Nager dans le bonheur
Une petite nageuse, quelques paillettes et des mots qui font du bien
Deux primitives — Button et Tag — et les objets qui les composent : le Nav, la fiche, la carte d'étude de cas avec son en-tête et son carrousel, l'intro d'une page d'accueil, et la pile qui empile les études de cas. Tous construits uniquement à partir des tokens. Ce qui est montré ici est rendu par les composants eux-mêmes — pas par une copie de leur markup. La doctrine d'usage vient des descriptions Figma : c'est elle qui porte les interdits.
Ce qu'il est. La chose sur laquelle on clique. Dans une maquette, un vrai contrôle ; dans un document ou sur une slide, la représentation d'une action.
Ce qu'il n'est pas. Ni une étiquette — une pilule qui ne fait rien est un Tag — ni un objet qu'on répète : deux boutons primaires dans une vue, et aucun des deux n'est principal. Pour naviguer à l'intérieur du Nav, c'est un lien qu'il faut, pas un bouton.
Description du composant Figma 21:47.
Le bouton rond est le secondaire sans libellé. Même fond (color.bg.surface, puis accent 100 et 200), même encre ; seule la boîte change : sa largeur vaut sa hauteur, et le rayon de la pilule en fait un cercle. Les trois diamètres sont les trois hauteurs du bouton, donc les cibles tactiles de WCAG 2.5.5. Comme il n'a pas de texte, il ne se compile pas sans icon ni aria-label : un contrôle expose un nom (WCAG 4.1.2).
Aucune des trois variantes n'a de liseré, et c'est ce que Figma dessine : le secondaire se pose par son fond, color.bg.surface, et rien d'autre. Le trait qu'il portait jusqu'ici n'avait pas de source — il venait du portfolio, entré avec l'extraction du système, et valait 1,28:1 sur ce même fond quand WCAG 1.4.11 en demande 3. C'est ce qui explique qu'il ait tenu si longtemps sans que personne le voie.
L'écart entre deux boutons est --button-target-spacing (WCAG 2.5.8). Tabulez pour voir l'anneau de focus, et survolez pour voir le fond changer en --motion-duration-quick.
L'alerte porte une action destructive, et rien d'autre. C'est la seule variante qui ne lise pas la rampe de marque : ses trois fonds viennent de color.feedback.danger — 500 au repos, 600 au survol, 700 à l'appui, exactement comme le primaire lit accent 500/600/700. Les trois crans tiennent le libellé blanc à 4,8:1, 6,5:1 et 8,3:1 : tous au-dessus des 4,5:1 que WCAG 1.4.3 demande à 14 px. Un bouton rouge qui ne détruit rien apprend à ne plus être lu — c'est la seule raison de ne pas s'en servir ailleurs.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'alert' | 'icon' | primary | Primary porte l’action principale ; une seule par vue. Alert porte une action destructive. Icon est le bouton rond : l’icône seule, sans libellé, et il exige icon et aria-label. |
size | 'sm' | 'md' | 'lg' | md | Les trois hauteurs des tokens button.height.*. |
href | string | — | Avec href, rend un <a> ; sans, un <button>. |
icon | IconName | — | Nom d’une icône du système. Posée avant le label. Obligatoire avec variant="icon". |
iconEnd | IconName | — | Idem, mais posée après le label. Cumulable avec icon. |
aria-label | string | — | Le nom du bouton quand aucun libellé ne le porte. Obligatoire dès que l’icône est seule : la variante icon refuse de se compiler sans. |
type | 'button' | 'submit' | button | Ignoré quand href est fourni. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
Chaque taille pose ses variables locales (--btn-h, --btn-px, --btn-gap, --btn-icon, --btn-min-w) et chaque variante ses trois fonds (--btn-bg, --btn-bg-hover, --btn-bg-active) : un appelant peut en surcharger un ponctuellement via style plutôt que d'inventer une taille ou une variante de plus. C'est ce que fait le Nav pour son bouton d'action, que Figma compose en hauteur lg avec le padding horizontal md — et le Carousel pour ses flèches, que Figma pose sur color.bg.default et non sur la surface lavande du secondaire.
L'icône prend la couleur du bouton, parce qu'elle est inlinée et ne connaît que currentColor. icon et iconEnd attendent un nom du registre, pas un chemin : la même flèche sert au primaire et au secondaire. Le jeu complet est sur Icônes.
Quatre compositions, quelle que soit la taille et quelle que soit la variante. C'est la place de l'icône qui les distingue, et elle se choisit par le nom de la prop — il n'y a pas de valeur left ou right à passer.
<Button>Texte seul</Button>
<Button icon="mail-edit">Icône à gauche</Button>
<Button iconEnd="arrow-narrow-right">Icône à droite</Button>
<Button variant="icon" icon="trash" aria-label="Supprimer" />Les deux se cumulent : icon et iconEnd ensemble encadrent le libellé. Le quatrième cas est la variante icon, et elle porte elle-même les deux précautions qu'il fallait connaître : sans libellé, plus rien ne donne sa largeur au bouton — elle retire donc sa largeur minimale — et une icône ne s'annonce pas : aria-label devient le seul nom du bouton, faute de quoi un lecteur d'écran dit « bouton » et rien de plus. Elle refuse de se compiler sans lui.
Deux appelants écrivent encore l'ancienne recette. Les flèches du Carousel et la page Icônes retirent la largeur minimale à la main. Le Carousel pose en plus color.bg.default à la place de la surface lavande, ce que la variante n'a pas : ils n'ont pas été touchés, et la bascule se décide avec eux.
Écart à arbitrer côté Figma. Les trois familles height, padding-y et icon-size sont sur-déterminées et ne s'accordent pas : en taille md, 16 + 24 + 16 = 56 px alors que --button-height-md vaut 48. Seule la taille sm est cohérente. Ici la hauteur prime — c'est la dimension que les descriptions rattachent à WCAG 2.5.5 — et le padding vertical n'est pas appliqué.
Ce que Figma déclare et que le code n'a pas. Le composant Figma porte quatre variantes (Primary, Secondary, Ghost, CTA) et quatre états (Default, Hover, Pressed, Disabled). Le code implémente deux variantes et les quatre états : ni Ghost ni CTA. À intégrer le jour où une vue en a besoin — pas avant. L'état désactivé, lui, est arrivé avec les flèches du carrousel : Figma ne change pas le fond, il baisse l'opacité de tout le bouton à 45 % (21:11, 21:26). Le contraste passe alors sous le seuil — WCAG 1.4.3 exempte les contrôles inactifs.
Les 22 tokens button.* : hauteur, padding, gap, taille d'icône, largeur minimale, rayon, anneau de focus. Ils décrivent cet objet et pas le système — c'est pour ça qu'ils sont ici plutôt que dans Fondations.
--button-radius — 999px ← --radius-pillButton border radius. Aliases radius/pill for full-rounded buttons.
--button-focus-offset — 3pxFocus ring offset (3px). WCAG 2.4.13 — visible separation from button edge.
--button-focus-ring-width — 3pxFocus ring width (3px). Exceeds WCAG 2.4.13 minimum (2px) for maximum visibility.
--button-target-spacing — 24px ← --space-xlMinimum spacing between adjacent buttons (24px). WCAG 2.5.8 Target Spacing — ensures no accidental activation of neighboring targets.
--button-height-sm — 44pxSmall button height (44px). Meets WCAG 2.5.5 AAA minimum touch target.
--button-height-md — 48pxMedium button height (48px). Meets Apple HIG & Material 3 recommended minimum.
--button-height-lg — 56pxLarge button height (56px). Maximum comfort, exceeds all guidelines.
--button-height-xs — 36px--button-padding-x-sm — 16px ← --space-lgSmall button horizontal padding (16px). Ensures text never crowds edges.
--button-padding-x-md — 24px ← --space-xlMedium button horizontal padding (24px). Comfortable reading area.
--button-padding-x-lg — 32px ← --space-2xlLarge button horizontal padding (32px). Maximum legibility.
--button-padding-y-sm — 12px ← --space-mdSmall button vertical padding (12px). Ensures 44px minimum height with text.
--button-padding-y-md — 16px ← --space-lgMedium button vertical padding (16px). Comfortable for 48px height.
--button-padding-y-lg — 24px ← --space-xlLarge button vertical padding (24px). Maximum comfort for 56px height.
--button-gap-sm — 8px ← --space-smSmall button icon-to-label gap (8px). Clear separation for cognitive accessibility.
--button-gap-md — 12px ← --space-mdMedium/large button icon-to-label gap (12px). Generous spacing for clarity.
--button-icon-size-sm — 20pxSmall button icon (20px). Above WCAG minimum for perceivable graphics.
--button-icon-size-md — 24pxMedium button icon (24px). Meets WCAG AAA for interactive graphic elements.
--button-icon-size-lg — 28pxLarge button icon (28px). Exceeds all guidelines for maximum visibility.
--button-min-width-sm — 44pxSmall button minimum width (44px). WCAG 2.5.5 AAA in both dimensions.
--button-min-width-md — 96pxMedium button minimum width (96px). Ensures comfortable label space.
--button-min-width-lg — 140pxLarge button minimum width (140px). Maximum legibility and tap confidence.
Ce qu'il est. Un texte qui mène quelque part, sans fond : l'action la plus discrète d'une vue, avec une icône à droite si l'on veut.
Ce qu'il n'est pas. Ni un Button — il n'a ni fond, ni taille, ni variante, et l'action principale d'une vue n'est jamais un lien — ni un texte coloré : sans son soulignement au survol, rien ne le distinguerait d'un mot posé là.
Composant Figma 613:4816.
Le survol souligne, l'appui grise. Au repos le libellé est à color.text.default, sans trait ; au survol il est souligné, sans changer de couleur ; à l'appui il passe à color.text.muted, sans soulignement. Le soulignement est le seul signe qui sépare un lien d'un texte (WCAG 1.4.1 : la couleur seule ne suffit pas). Tabulez pour voir l'anneau de focus, qui entoure la zone cliquable et non le texte.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
href | string | — | Avec href, rend un <a> ; sans, un <button>. |
iconEnd | IconName | — | Nom d’une icône du système, posée après le libellé. |
type | 'button' | 'submit' | button | Ignoré quand href est fourni. |
disabled | boolean | false | Sans href seulement : un <a> ne se désactive pas. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
style | string | — | Surcharge ponctuelle : --link-color, --link-color-active. |
Écart à arbitrer côté Figma. Le dessin pose space.md tout autour du texte : avec l'icône de 24 px le lien mesure 48 de haut, mais sans elle il n'en fait que 41. Le code ajoute min-height: var(--button-height-sm) (44) pour que la cible tienne dans les deux cas — le composant Figma ne porte pas cette hauteur minimale.
Ce qu'il est. Une étiquette courte qui classe la chose posée à côté d'elle : un statut dans une ligne de tableau, un mot-clé sur une slide, une catégorie dans un document — ici, les méthodes d'une étude de cas.
Ce qu'il n'est pas. Ni un bouton — il n'a aucun état interactif, et une pilule sur laquelle on clique est un Button — ni une phrase, ni le seul porteur d'un sens : c'est le texte qui doit dire ce qu'il veut dire, pas la couleur.
Description du composant Figma 19:12.
Les trois remplies tiennent par une seule règle : fond au cran 100 d'une rampe, encre au cran 800 de la même rampe. La paire ne se dissocie pas — c'est elle qui porte le contraste, et chacune dépasse 8:1. La quatrième n'a pas de fond : liseré color.action.primary.bg, encre color.text.accent. C'est tout le jeu : il n'y a pas de cinquième couleur à composer, et un tag n'a jamais deux couleurs à la fois.
Le défaut est outline, là où Figma ouvre le jeu sur Accent. C'est la variante que portent 42 des 49 tags du fichier, et la seule qu'une vue d'ici utilise — la carte d'étude de cas, plus bas. Prendre le défaut de Figma l'aurait repeinte sans le dire.
Le liseré est un outline rentré d'un pixel, pas un border. Dans Figma le contour est tracé en dedans et la pilule mesure 33 px de haut ; un border s'ajoute au padding et l'aurait portée à 35. L'outline ne compte pas dans la boîte — les quatre variantes ont donc exactement la même hauteur, et une rangée qui les mélange ne bouge pas.
Le texte passe par le slot par défaut.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
variant | 'accent' | 'lime' | 'neutral' | 'outline' | outline | Les quatre variantes de Figma. Les trois premières sont remplies. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
style | string | — | Le point de surcharge : --tag-color sur l’outline, et rien d’autre. |
Un seul levier, et il ne vaut que pour l'outline. --tag-color reprend d'un coup le liseré et l'encre — jamais l'un sans l'autre. Elle se pose en style sur un tag, ou sur un ancêtre pour toute une rangée, et le repli reste color.action.primary.bg. Le dessin, la graisse, le rayon et l'espacement ne se reprennent pas : un tag qui change de dessin n'est plus un tag.
Sur une variante remplie, elle ne fait rien — elle n'y toucherait que l'encre, donc déferait la paire 100/800 et le contraste qui la justifie. Une couleur qui n'est pas au catalogue se demande à Figma ; elle ne se pose pas ici.
La valeur attendue est le cran 800 d'une rampe, celui que Figma donne déjà à l'encre des variantes remplies. C'est le seul cran qui tienne 4,5:1 sur les trois fonds du système : au pire, sur color.bg.section, accent 800 mesure 11,2:1, neutral 800 9:1 et highlight 800 6,7:1 (WCAG 1.4.3) — le liseré suivant la même couleur, 1.4.11 passe par surcroît. Les crans plus clairs ne tiennent pas : highlight 700 tombe à 4,2:1 sur cette même surface, et color.feedback.danger.solid à 3,9:1.
Écart à arbitrer côté Figma. La deuxième variante s'appelle Lime dans le fichier, mais la rampe qu'elle consomme s'appelle highlight — highlight.100 et highlight.800. Le code a gardé le nom de la variante pour que la prop se recoupe avec Figma, et se retrouve donc à écrire lime pour lire highlight. Un nom de couleur pour une variante, un nom de rôle pour la rampe : c'est la variante qu'il faut renommer.
Ce qu'il est. La barre en haut d'une page : six boutons — l'avatar de retour à l'accueil, les quatre items, et l'action.
Ce qu'il n'est pas. Ni un en-tête de section à l'intérieur d'une page — ça, c'est un titre — ni un menu : au-delà de cinq liens environ, une navigation devient autre chose et demande un autre dessin.
Pas de filet sous la barre : ce système sépare ses sections par du blanc.
Écart à arbitrer côté Figma. La description du composant (24:5) décrit encore l'ancienne barre — « a wordmark on the left » — et affirme que le survol et la page courante partagent la même couleur (color.text.accent). Ni l'un ni l'autre n'est vrai du composant actuel : il porte un avatar, et l'état courant est une pastille color.bg.default. Le texte ci-dessus ne reprend donc que ce qui vaut encore. La description est à réécrire dans Figma.
Le masquage n'est dessiné nulle part. La section Home de la page Templates (ancienne page HP) porte trois états de défilement (45:388 au repos, 222:907 et 222:1042 les cartes montées), et aucun ne montre la barre effacée. Aucune réaction de prototypage n'y est posée non plus. Il manque un quatrième état — barre effacée, première carte par-dessus l'intro — sans quoi ce code est la seule spécification du mouvement.
La barre n'a pas de fond, et une carte a le même que sa pilule. Rien n'est dessiné sous la barre : ce qui flotte au-dessus du contenu, ce sont trois objets déjà opaques — l'avatar, la pilule et le bouton. Mais la carte d'étude de cas est en color.bg.surface, exactement le lavande de la pilule : quand une carte passe sous la barre révélée, la pilule perd son contour et il ne reste que la pastille blanche de l'onglet courant. Et la carte pose son eyebrow — l'année et le client — à 64px de son haut, sous une barre qui en fait 88 : il passe entièrement derrière elle. Mesuré, pas supposé. À trancher côté Figma : soit le composant reçoit un fond, soit on acte que la barre flotte et que ces deux états sont voulus.
Ils ne sont pas une variante à déclarer : ils tombent des props. Sans home, pas d'avatar ; si current ne correspond à aucun lien, aucun item n'est actif.
Pas d'avatar : on n'offre pas un retour vers la page où l'on est. Aucun item courant : l'accueil n'est pas un des quatre.
L'avatar ramène à l'accueil, l'item de la page porte la pastille. Survolez un autre onglet : elle s'y rend en --motion-duration-default, puis revient.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
links | { href, label, icon? }[] | — | Les items. Le composant ne les connaît pas d’avance. |
current | string | — | Chemin courant. Aucun item actif s’il ne correspond à aucun href. |
home | { href, src, alt } | — | Le bouton avatar. Absent ⇒ non rendu : c’est l’état d’accueil. |
action | { label, icon?, href? } | — | Le sixième bouton. Sans href ⇒ <button> inerte. |
label | string | Navigation principale | aria-label du <nav>. |
sticky | boolean | false | La barre tient le haut de l’écran et s’efface quand on descend. Absente, elle reste posée à plat — l’état montré ici. |
Avec sticky, la barre tient le haut de l'écran, glisse vers le haut quand on descend et revient quand on remonte. Le comportement est le même à toutes les largeurs : il ne dépend d'aucun point de rupture.
Il n'y a pas de démonstration ici, et ce n'est pas un oubli : une barre qui se juge sur le défilement d'une page entière ne se montre pas dans une section de doc, et elle lit scrollY — dans une boîte qui défile, elle ne bougerait pas. Elle est en situation sur le gabarit.
La barre doit être hors du bloc qu'elle surplombe, et il faut les deux raisons pour comprendre pourquoi. Un élément collé est un contexte d'empilement : rien de ce qu'il contient ne peut se peindre devant ce qui le recouvre, donc une barre restée dedans serait condamnée à passer sous les cartes, quel que soit son z-index. Et Case Study List rend ce bloc inerte quand il est recouvert, ce qui sortirait ses onglets du clavier au moment même où elle est la seule chose encore visible.
<Nav sticky … /> <!-- hors de .home-first, pas dedans -->
.home { --fold-peek: 90px; }
.home-first { position: sticky;
top: var(--nav-height);
height: calc(100svh - var(--fold-peek) - var(--nav-height)); }top: var(--nav-height) et non 0 : sinon l'intro glisserait derrière une barre qui n'a pas de fond pendant les cent premiers pixels, avant que le masquage ne s'enclenche. La barre reste dans le flux — un élément collé garde sa place — donc le premier écran fait toujours 100svh moins le liseré en tout, et celui-ci ne bouge pas d'un pixel.
La page ne déclare pas --nav-height, elle la lit. C'est un token comme un autre — nav.height, 88 dans les modes Desktop et Tablet, 72 dans Mobile — donc une décision de dessin qui se prend dans Figma et se ré-exporte, pas un calcul posé dans une page. Une page cale son premier écran sous la barre sans rien savoir de ce qui la compose. La hauteur se lit et ne se mesure pas : la mesurer en JavaScript ferait sauter la carte au chargement, pour la raison déjà écrite sur la pile. Les ancres, elles, peuvent l'être : le composant pose lui-même scroll-padding-top sur la page, parce que ce décalage n'a de conséquence qu'au moment où l'on suit un lien, et que rien ne bouge au chargement.
| Variable | Défaut | Rôle |
|---|---|---|
--nav-height | 88px — 72px sous 768 | Le token nav.height, en deux modes. À lire, jamais à écrire : c’est lui qui cale le premier écran d’une page sous la barre. |
--nav-hide-after | 100px | Le seuil sous lequel la barre ne s’efface jamais, quel que soit le sens du défilement. |
--nav-hide-grain | 6px | En deçà, un mouvement n’est pas un geste : c’est ce qui empêche le trackpad et l’inertie iOS de faire clignoter la barre. |
--nav-shift | 0 | Le déplacement de la barre. Écrit par l’état, pas à la main — on ne le pose que pour figer la barre à des fins de capture. |
Le sens du défilement est du JavaScript, le mouvement non. Le script compare deux positions de scrollY d'une image à l'autre, jamais un geste. Un wheel dirait la main ; il ne dirait rien d'un saut — touche Origine, clic sur une ancre, scrollTo — qui ramène la page en haut sans que personne n'ait touché la molette. Deux positions comparées disent les deux : remonter, c'est remonter, quelle qu'en soit la cause. C'est aussi ce qui rend le tactile et la souris identiques sans écrire un seul cas particulier.
La barre masquée reste atteignable au clavier, et le focus l'épingle. Elle n'est jamais rendue inert : inerte, elle ne pourrait plus recevoir le focus, donc plus jamais se révéler en le recevant, et le seul repère de navigation de la page disparaîtrait des lecteurs d'écran sur un critère de position de défilement. Épinglée plutôt que simplement révélée, parce qu'une barre seulement révélée se remasquerait au défilement suivant, l'anneau de focus avec elle — c'est ce que WCAG 2.4.11 interdit. La pile résout le même problème par l'autre bout : là on retire ce qui est couvert, ici on découvre ce qu'on atteint.
Elle écrit le seul z-index du système. Partout ailleurs les couches se peignent dans l'ordre du DOM, et c'est ce qu'on veut : la carte écrite après recouvre celle d'avant. La barre est le cas contraire — écrite avant tout le reste, elle doit se peindre après, sans quoi la révéler derrière une carte opaque ne révélerait rien. 1, et non 10 : le plus petit nombre qui suffit, et qui ne laisse pas croire que le système a une échelle d'élévation. Il n'en a pas.
Le seuil et le grain ne sont pas des valeurs de dessin. Le seuil de 100px vient de la consigne ; le grain de 6px est une tolérance d'appareil, de la même famille que les 2px que la pile accorde à l'arrondi sous-pixel. Aucun token ne les porte, et c'est volontaire : Figma ne dessine pas l'inertie d'un trackpad.
Elle a un repli. Sans JavaScript, sticky donne une barre qui colle et ne s'efface jamais. On perd le mouvement, pas la navigation.
La pastille est du JavaScript. Le script mesure l'onglet visé et écrit sa géométrie en variables CSS ; le mouvement, lui, reste au CSS. Elle suit le survol et le focus clavier.
Elle a un repli. Sans JavaScript, l'onglet courant garde son fond blanc et le survol son aplat lavande — le comportement d'avant la pastille. C'est le script qui pose data-slider-ready et neutralise ces deux aplats.
Sous 768px, les icônes et le bouton d'action s'effacent, l'avatar reste — il est le chemin de retour — et la pilule défile si les onglets ne tiennent plus.
Sous prefers-reduced-motion, la pastille se repositionne d'un coup : ce sont les durées des tokens qui tombent à zéro, le composant n'a rien à savoir de la préférence.
Ce qu'elle est. Une composition de carte de liste : elle assemble la primitiveCard, le texte, l'image, les tags et les actions. Elle décide de son agencement selon la largeur de la carte et devient entièrement cliquable quand le slot actionsest absent.
Ce qu'elle n'est pas. Ni la grille qui pose les colonnes, ni une carte métier, ni la primitive neutre : les textes, les liens, les images et les états propres à la page restent des données fournies par l'appelant.
Figma 451:1004. La carte réagit à sa propre largeur avec quatre paliers de container query ; le quatrième utilise une image portrait dont la hauteur suit les informations.
Sans actions, le titre porte un lien qui couvre toute la carte ; avecactions, seuls les boutons sont interactifs. Les conteneurstags-top, tags-bottom et actions disparaissent quand leur slot est absent.
Une fête miniature à déplier, à installer et à célébrer. Même pour rien. Surtout pour rien.
La grille appartient à la page : elle pose ses colonnes et transmet seulement la largeur disponible à chaque carte. Les œuvres CSV exposent volontairement des titres, descriptions, images et listes de techniques de longueurs variées. Redimensionnez chaque carte pour observer ses quatre paliers ; la grille ne les connaît pas.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
href | string | — | La page de détail. Obligatoire sans actions ; interdite avec actions. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
style | string | — | Style transmis à la carte racine. |
tags-top ouvre la rangée au-dessus du titre ; absent, elle disparaît sans laisser d’espace. tags-bottom ouvre la rangée sous les informations ; absent, aucun conteneur n’est rendu. actions ouvre les actions à droite ; absent, la carte entière devient le lien porté parhref. Les trois slots sont indépendants et conditionnels.
CardDefault lit les tokens card.* pour ses trois lignes de texte. Les ratios d'image et les seuils de largeur restent respectivement des tokens ratio.* et des bornes structurelles de container query ; ils ne sont pas recopiés dans cette page.
| Token | Valeur CSS | Rôle |
|---|---|---|
--card-title-size | var(--font-size-title) | Card title size. Aliases font/size/title — the same step an H2 takes. Named so the text of a card can be retuned without moving page headings. |
--card-title-color | var(--color-text-default) | Card title ink. Aliases color/text/default. 15.3:1 on color/bg/default, which is what a card is painted with. |
--card-subtitle-size | var(--font-size-body-lg) | Card subtitle size. Aliases font/size/body-lg — one step above body, so the line under the title reads before the secondary one. |
--card-subtitle-color | var(--color-text-default) | Card subtitle ink. Aliases color/text/default: the subtitle carries meaning, so it reads at full strength and is not muted. |
--card-meta-size | var(--font-size-body) | Card secondary info size. Aliases font/size/body — a date, a place, an author. Read after the subtitle, so it does not shrink below body. |
--card-meta-color | var(--color-text-muted) | Card secondary info ink. Aliases color/text/muted. 5.56:1 on color/bg/default — the only quiet step, and it stays above 4.5:1 on the card. |
Ce qu'elle est. Une fiche qui présente une chose : son image, une rangée de tags, son titre, son sous-titre, une ligne d'informations secondaires, puis deux autres emplacements libres — une seconde rangée de tags et les actions. Verticale ou horizontale, c'est la même fiche.
Ce qu'elle n'est pas. Ni la carte d'étude de cas — celle-là est une feuille pleine largeur qui monte du bas de la page, avec sa phrase d'impact et son carrousel, et elle est plus bas — ni un bouton : la carte entière ne se clique pas, et rien n'y est un lien. Ce qui mène quelque part se pose dans l'emplacement actions, et c'est un Button.
Figma 334:1129. Le composant n'a pas de description : ce qui précède est lu sur le dessin, pas sur le fichier — voir plus bas.

Faster deployments — Shared UX language — Roadmap alignment • User trust and satisfaction
Verticale, l'image est en tête et prend toute la largeur de la carte. Horizontale, elle occupe la moitié gauche et le texte se centre en face, sur sa hauteur. C'est la seule différence : mêmes marges, mêmes espacements, même dessin du texte. Le contenu ci-dessus et ci-dessous est le même.

Faster deployments — Shared UX language — Roadmap alignment • User trust and satisfaction
La carte prend la largeur qu'on lui donne — c'est la page qui décide, comme pour la carte d'étude de cas. Les deux cadres ci-dessus ne montrent donc pas une largeur du composant mais celle que Figma lui dessine : 337 pour la verticale, le plafond de 768 pour l'horizontale.
Sous 768px, l'horizontale repasse en colonne. Figma la borne à 560 de large au minimum, et deux colonnes ne tiennent pas dans un téléphone de 390. Ce cas n'est pas dessiné : c'est une décision du code, prise au seul point de rupture du système.
Le titre descend d'un cran quand la carte n'est pas la chose la plus grosse de la page. titleSize="title" est le défaut et la maquette : la taille d'un h2, 2rem. titleSize="heading" prend celle d'un h4, 1.25rem — pour une grille serrée, ou sous un titre de page qui doit rester devant.
Seule la taille change. La police, la graisse, l'interligne et l'interlettrage suivent d'eux-mêmes : l'interligne est un ratio et l'interlettrage est en em, donc les deux se recalculent sur la nouvelle taille. Un titre en heading est exactement un h4 du système, et pas un titre de carte rapetissé.
Ce n'est pas headingLevel. Le rang dit où le titre se range dans le plan du document, la taille dit ce qu'on voit. Les deux sont restés séparés depuis le début — une carte peut être un h2 en petit, ou un h4 en gros, et rien dans le composant ne les lie.

Faster deployments — Shared UX language — Roadmap alignment • User trust and satisfaction
La bascule ne réécrit pas la taille du titre, elle repose --card-title-size sur la carte. L'endroit qui décide reste donc unique, et l'alias card.title.size sert enfin à ce pour quoi il avait été gardé : être le point où la typographie d'une carte bouge sans emporter les titres de page.
Les tags et les actions ne sont pas des props, ce sont des slots nommés — les mêmes que dans Figma. La carte ne pose que la rangée qui les tient : les tags se replient sur plusieurs lignes, les actions se calent à droite. Ce qu'on y met se dessine tout seul.
Les tags ont deux rangées, et seule leur place les distingue. tags-top passe au-dessus du titre, avant tout le texte ; tags reste sous les informations secondaires. Le dessin des deux est exactement le même — même rentrée de 16, même écart de 8, même repli — et c'est voulu : deux rangées de pilules qui ne se ressembleraient pas seraient deux objets, pas un emplacement dédoublé.
Ce qui va dans l'une plutôt que dans l'autre reste à écrire. La maquette met les mêmes tags aux deux endroits, donc elle ne tranche pas ; la doc, elle, montre une catégorie en haut et les méthodes en bas, parce que c'est la lecture la plus évidente de deux rangées superposées. Ce n'est pas une règle que le composant tient — c'est une doctrine, au même titre que « des Tag et rien d'autre ».
Un emplacement vide n'existe pas : sans tag, la rangée des tags n'est pas rendue, et l'espacement du corps ne lui compte pas de place. C'est ce que dit Figma — displayEmptyByDefault: false.
Figma restreint l'emplacement des tags aux Tag et laisse celui des actions ouvert. Le code ne peut pas imposer la première règle ; elle reste une doctrine. Dans le fichier, l'action est un Button secondaire de taille sm, réduit à sa seule flèche.
Seul le titre est obligatoire. Sans image, la carte ne réserve pas la place — elle commence au texte. Sans sous-titre, sans ligne secondaire, sans tags, sans actions : la ligne n'est pas rendue, et rien ne laisse un trou. Une carte réduite à son titre est une carte valide.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
orientation | 'vertical' | 'horizontal' | vertical | Les deux variantes de Figma. Verticale, l’image est en tête ; horizontale, elle occupe la moitié gauche. |
title | string | — | Le titre. La seule prop obligatoire. |
titleSize | 'title' | 'heading' | title | La taille du titre : title est celle d’un h2 — ce que Figma dessine — heading celle d’un h4. Sans rapport avec headingLevel, qui règle le rang. |
subtitle | string | — | La ligne sous le titre. |
meta | string | — | L’information de second plan, sous le sous-titre — une date, un lieu, un auteur. Absente ⇒ la ligne n’est pas rendue. |
image | { src, alt, width, height } | — | L’image de tête. width et height donnent le rapport de l’image, pas la taille de la boîte. Absente ⇒ la carte n’en réserve pas la place. |
headingLevel | 2 | 3 | 4 | 5 | 2 | Le rang du titre, sous ce que la page porte déjà. Il ne change pas le dessin. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
style | string | — | Le point de surcharge : --card-media-height, et rien d’autre. |
| Slot | Rôle |
|---|---|
tags-top | La rangée de tags au-dessus du titre, avant tout le texte. |
tags-bottom | La seconde rangée, sous les informations secondaires. Figma n’accepte que des Tag dans les deux — le code ne peut pas l’imposer, la doctrine le dit. |
actions | Ce qui mène quelque part : un Button, ou plusieurs. Ils se calent à droite de la carte. |
C'est la carte qui donne sa place à l'image, jamais le rapport du fichier. object-fit: cover est ce qui rend ça tenable : l'image est recadrée, jamais déformée, et deux photos de rapports différents font exactement la même bande. Une carte ne change donc pas de dessin selon la photo qu'on y met.
Horizontale, les deux colonnes remplissent la carte — l'image et le bloc de contenu font sa hauteur, l'une comme l'autre. Le contenu, lui, se centre dans sa colonne : un titre court ne se colle pas en haut d'une carte haute, il reste au milieu. C'est exactement ce que Figma pose sur ce bloc.
La hauteur vient du plus haut des deux : le plancher de l'image, ou le contenu du bloc quand il le dépasse. Une carte dont le titre se replie sur trois lignes a donc une image aussi haute que son texte, et non une image de 256 centrée entre deux bandes vides.
Verticale, l'image est ce qui donne et reprend la hauteur. Sans consigne, elle vaut celle de Figma. Si une page impose une hauteur à la carte, c'est l'image qui absorbe le surplus ou qui se resserre — le texte, lui, ne bouge pas. C'est la même règle que la carte d'étude de cas, où l'en-tête est le point fixe et le carrousel cède. Une grille de cartes à hauteur égale aligne donc ses images sans qu'on ait à mesurer quoi que ce soit.
Un seul levier. --card-media-height vaut 324px en vertical et 256px en horizontal. Elle se pose en style sur une carte, ou sur un ancêtre pour toute une grille. Le reste ne se reprend pas — une carte qui change de marges, de rayon ou d'ombre n'est plus cette carte.
C'est le seul réglage du système dont le sens change d'une orientation à l'autre, et c'est assumé. Verticale, c'est une hauteur : celle que l'image prend quand rien ne la contraint. Horizontale, c'est un plancher : la hauteur vient du texte d'en face, et cette valeur empêche seulement une carte réduite à son titre d'écraser son image sur deux lignes.
Les 6 tokens card.* : la taille et l'encre des trois lignes de texte de la carte. Ils décrivent cet objet et pas le système — c'est pour ça qu'ils sont ici plutôt que dans Fondations.
Ce sont six alias, et c'est voulu. Aucun ne porte de valeur à lui : chacun renvoie à une primitive que le système avait déjà — card.title.size lit font.size.title, qui est aussi la taille d'un h2. Le token n'ajoute donc pas une valeur, il ajoute un endroit : celui où la typographie d'une carte peut bouger sans emporter les titres de page avec elle. C'est ce que fait déjà le groupe button avec button.radius, qui n'est qu'un alias de radius.pill.
Ils ne fixent que la taille et l'encre. La police, la graisse, l'interligne et l'interlettrage du titre restent ceux d'un h2, posés par global.css pour tous les rangs : les nommer ici aurait détaché les titres de carte de l'échelle typographique du système, ce qui est exactement ce qu'un design system ne doit pas laisser arriver par distraction.
--card-title-size — 2rem ← --font-size-titleCard title size. Aliases font/size/title — the same step an H2 takes. Named so the text of a card can be retuned without moving page headings.
--card-title-color — #2F2D2E ← --color-text-defaultCard title ink. Aliases color/text/default. 15.3:1 on color/bg/default, which is what a card is painted with.
--card-subtitle-size — 1.0625rem ← --font-size-body-lgCard subtitle size. Aliases font/size/body-lg — one step above body, so the line under the title reads before the secondary one.
--card-subtitle-color — #2F2D2E ← --color-text-defaultCard subtitle ink. Aliases color/text/default: the subtitle carries meaning, so it reads at full strength and is not muted.
--card-meta-size — 0.9375rem ← --font-size-bodyCard secondary info size. Aliases font/size/body — a date, a place, an author. Read after the subtitle, so it does not shrink below body.
--card-meta-color — #6A676D ← --color-text-mutedCard secondary info ink. Aliases color/text/muted. 5.56:1 on color/bg/default — the only quiet step, and it stays above 4.5:1 on the card.
La carte d'étude de cas ne les lit pas. Son titre et sa phrase d'impact portent pourtant les mêmes valeurs, et c'était l'autre partage possible. Le groupe a été gardé sur la fiche seule : les deux objets pourront donc diverger, et si l'un doit suivre l'autre, c'est une décision à reprendre — pas un oubli.
Le composant n'a pas de description. C'est d'elle que sortent normalement le « ce qu'elle est » et le « ce qu'elle n'est pas » ; ici la doctrine ci-dessus est déduite du dessin et du reste du système. Elle n'engage donc que le code, et c'est l'écart le plus cher de cette section : deux personnes qui lisent le même dessin n'en tirent pas les mêmes interdits. Une description dans Figma les trancherait pour de bon.
Deux noms à reprendre. Le composant s'appelle Cards au pluriel, avec une espace finale — un nom de tiroir, pas un nom d'objet : le code le nomme Card. Et la propriété qui commande les deux variantes s'appelle Property 1, ce qui ne dit rien de ce qu'elle fait : le code la nomme orientation. Les deux se renomment côté Figma.
La seconde taille de titre n'est pas dessinée. Le fichier ne connaît que heading/title sur ce nœud — 334:1122, lié à card/title/size — et titleSize="heading" n'a donc aucun frame qui le montre. C'est une décision du code, comme le passage en colonne sous 768px : la maquette n'est pas en retard, elle n'a simplement jamais eu à trancher ce cas. Elle le deviendrait le jour où une vue publie les deux tailles côte à côte — c'est ce jour-là qu'un second frame vaudrait la peine, pas avant.
La rangée de tags du haut n'est pas un slot. Dans le fichier, c'est un frame nommé Frame 6 — 392:1075 en vertical, 392:1076 en horizontal — qui contient deux instances de Tag, là où la rangée du bas est un vrai slot nommé Tags. L'emplacement n'est donc pas remplissable dans la maquette, et il n'a pas de nom : tags-top est une décision du code, comme orientation l'a été. Le geste tient en deux réglages — convertir le frame en slot, le nommer.
Et les deux rangées y portent le même contenu témoin, quatre fois Accent. Rien dans le dessin ne dit donc ce qui va en haut plutôt qu'en bas : la doctrine écrite plus haut est déduite, pas lue. C'est le même manque que la description absente, en plus petit.
Le fond blanc de la carte n'est lié à aucune variable. La valeur est exactement celle de color.bg.default, et c'est ce que le code écrit — mais rien dans le fichier ne le dit, et un changement de la variable ne repeindrait pas la maquette. Il manque une liaison, pas un token.
Les deux ombres sont des tokens. shadow/md pour la carte, shadow/lg pour l'image — la même que les vignettes du carrousel, et maintenant lue au même endroit par les deux. Figma n'ayant pas de type de variable « ombre », chaque morceau est nommé seul — décalage, flou, étendue, couleur — puis lié à la couche dans le fichier. npm run tokens les remet bout à bout. Le dessin et le code lisent donc la même valeur, et plus rien ne peut diverger.
Figma centrerait l'image là où le code l'étire. Dans le fichier, l'image de la carte horizontale est haute de 256 et la carte s'ajuste à son contenu : les deux tombent juste parce que le texte de la maquette fait exactement cette hauteur. Avec un titre de vraie longueur, l'auto-layout laisserait l'image à 256 et la centrerait entre deux bandes vides — ce que le code refuse de faire. L'écart ne se voit donc pas sur le nœud tel qu'il est dessiné aujourd'hui, et c'est bien pour ça qu'il est écrit ici. Le geste côté Figma tient en un réglage : passer la hauteur de l'image en fill, comme sa largeur.
La hauteur de l'image n'a pas de token — 324 en vertical, 256 en horizontal. Elle est hissée en --card-media-height, qui est aussi le point de surcharge public de la carte. Aucun groupe de tokens ne décrit une hauteur de média ; l'ouvrir pour deux nombres serait prématuré, mais un troisième le rendrait nécessaire.
L'écart entre deux actions vaut space.xs, là où l'en-tête d'étude de cas desserre le sien pour tenir WCAG 2.5.8. La règle n'est pas contredite : les boutons d'action mesurent au moins button.height.sm, soit 44px, et une cible de 44px satisfait 2.5.8 par sa taille sans rien demander à sa marge. Ce serait faux si l'emplacement recevait des cibles plus petites — c'est une raison de plus pour n'y mettre que des boutons.
Ce qu'elle est. Une étude de cas présentée en pleine largeur : l'année et le client, le titre, la phrase d'impact, les méthodes, le lien vers l'étude — puis les images du projet, qui défilent.
Ce qu'elle n'est pas. Ni une vignette dans une grille — les coins bas carrés et l'ombre projetée vers le haut disent une feuille qui monte depuis le bas de la page, pas une boîte posée au milieu — ni une galerie : le carrousel montre le travail, il ne le remplace pas. La carte ne connaît ni les études de cas ni leurs URLs ; tout tombe des props.
Figma 88:131, en situation 176:1248.
2025 - Rubika
Faster deployments — Shared UX language — Roadmap alignment • User trust and satisfaction



La carte prend la largeur de son conteneur : c'est la page qui décide si elle est pleine largeur, la carte ne le sait pas. Ici elle est bornée par le cadre de démonstration.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
eyebrow | string | — | L’année et le client. Mis en capitales par le CSS. |
title | string | — | Le titre de l’étude de cas. |
impact | string | — | La phrase d’impact, sous le titre. |
tags | string[] | [] | Les méthodes. Vide ⇒ la rangée n’est pas rendue. |
action | { label, href? } | — | Le lien vers l’étude. Sans href ⇒ <button> inerte. |
images | { src, alt, width, height }[] | [] | Les vignettes. Vide ⇒ pas de carrousel. |
imagesLabel | string | Images — {title} | Nom accessible de la piste. |
headingLevel | 2 | 3 | 4 | 5 | 2 | Le rang du titre, sous ce que la page porte déjà. Il ne change pas le dessin : la taille vient du composant. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
2025 - Rubika
Faster deployments — Shared UX language — Roadmap alignment • User trust and satisfaction
Le titre et le bouton partagent une ligne et s'alignent sur leur bas. Rétrécissez la fenêtre : sous 320px de titre, le bouton passe dessous plutôt que d'écraser le titre. Ses props sont celles de la carte, moins images et imagesLabel — la carte ne fait que les lui passer.



Les flèches sont posées par-dessus les images, collées aux deux bords et centrées sur la hauteur de la piste. Elles avancent d'une vignette et se désactivent en bout de piste. La piste défile aussi au doigt, au trackpad et au clavier — tabulez jusqu'à elle, puis flèches gauche et droite.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
items | { src, alt, width, height }[] | — | width et height donnent le rapport de la boîte, pas la taille du fichier : c’est le cadrage voulu, l’image le remplit en cover. |
label | string | Images du projet | Nom accessible de la piste. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
Les flèches sont une commodité. Elles sont masquées dans le HTML et c'est le script qui les révèle — puis les masque à nouveau si tout tient dans la largeur. Sans JavaScript la piste défile déjà, et sa barre de défilement reste visible : c'est alors la seule chose qui dit que ça défile.
Une barre posée par-dessus une zone qui défile doit se laisser traverser. Figma pose la nav en absolu sur toute la largeur de la piste, mais ne dit rien de ce qu'il advient des gestes : telle quelle, cette bande avalerait le défilement au doigt et à la molette au milieu des images — une panne qu'on ne voit pas en cliquant les flèches. Elle est donc en pointer-events: none, et chaque flèche le reprend pour son compte dans son attribut style, faute qu'un style scopé d'Astro atteigne la racine d'un composant enfant.
Le focus ne se perd pas. Quand une flèche se désactive sous le focus, le script le déplace sur l'autre plutôt que de le laisser retomber sur le <body>.
C'est le seul composant qui lit prefers-reduced-motion. Le behavior: 'smooth' du défilement n'est pas une durée : tokens.css ne peut pas le remettre à zéro comme il le fait des motion.duration.*. Le script bascule donc lui-même sur 'auto'.
Sous 768px, les marges de 48px tomberaient à la moitié d'un écran de 390 : elles passent à --space-xl, le titre à --font-size-subtitle et la piste à 272px, ce qui laisse aux vignettes les 240px qu'elles avaient déjà. Et les tags s'effacent, comme les icônes et l'action du Nav au même point de rupture : c'est ce que la maquette mobile dessine, et ce n'est pas qu'une économie de place. Trois méthodes qui se replient sur trois lignes dans une colonne de 390 pèsent 119px — un tiers de l'en-tête — pour une information de second plan, lue après le titre et l'impact. display: none et non un masquage visuel : ces tags ne font pas partie de la vue mobile, ils n'ont donc à rester ni dans l'ordre de tabulation ni dans l'arbre d'accessibilité.
La carte accepte qu'on lui impose sa hauteur. Par défaut elle fait la taille de son contenu — 735 px à 1280 px de large, ce que Figma dessine. --card-height, posée depuis l'extérieur, la borne : c'est ainsi que Case Study List lui fait remplir un écran entier, donc en recouvrir vraiment une autre. Une hauteur et non un plancher, sans quoi la carte grandirait au-delà de l'écran dès que son contenu dépasse, et son bas resterait hors d'atteinte une fois la carte collée en haut. Bornée, elle passe l'écart au carrousel — c'est lui qui prend ou rend la place, jamais l'en-tête.
Les ombres sont des tokens. shadow/lg sur les vignettes, shadow/lg-invert sur la carte. Les variables locales --card-shadow et --carousel-item-shadow restent, mais elles ne portent plus de valeur : elles donnent un nom au rôle, et évitent qu'une carte imbriquée dans une autre hérite de l'ombre du dessus — les deux composants emploient le même nom pour deux ombres différentes.
Les deux ombres de la carte divergent. Le composant 88:131 déclare 0 -30px 30px -14px ; l'instance d'exemple 176:1248 déclare 0 -22px 48px -12px. Le code suit le composant — c'est lui la spécification, l'autre est une surcharge locale. À réaligner côté Figma.
L'eyebrow contredit le système. Figma le dessine en Inter semibold ; global.css réserve cette forme — 13px, capitales, interlettrage large — au h6, en Poppins bold. Le code suit Figma et ne rend donc pas un h6 : ce n'est de toute façon pas un titre mais une ligne de contexte au-dessus du titre. À trancher côté Figma : ou l'eyebrow rentre dans le rang, ou le système reconnaît deux eyebrows.
Deux écarts d'espacement. Figma pose 33px entre les vignettes et 10px entre les tags. Aucun token ne porte ces deux valeurs : la première devient --space-2xl (32), la seconde --space-md (12) — on n'a jamais intérêt à resserrer l'écart entre deux cibles voisines (WCAG 2.5.8).
Le Slot Figma accepte tout, le carrousel n'accepte que des images. La piste prend un tableau items plutôt qu'un slot : la hauteur, le rayon et l'ombre des vignettes sont l'affaire du carrousel, et les styles scopés d'Astro n'atteignent pas un contenu slotté. Même coupure que le Nav, qui prend un tableau links. Le jour où une vignette n'est plus une image, c'est le composant qu'il faut rouvrir.
L'en-tête est le point fixe, le carrousel est ce qui bouge. Figma fige la piste à 442px — le Slot de 88:143, marges comprises — dont 410 pour les images. C'est la hauteur d'où l'on part. Mais dans une carte dont la hauteur est imposée, comme dans Case Study List où une carte fait exactement un écran, le carrousel prend ou rend tout ce qui reste, et les vignettes le suivent : c'est la piste qui porte la hauteur, l'image la remplit. L'en-tête, lui, ne bouge pas — il est protégé par la taille minimale automatique des boîtes flexibles, que le carrousel lève sur lui-même par min-height: 0. Montré seul, hors de toute carte, le carrousel garde ses 442.
Trois coquilles dans le texte de la maquette. L'exemple Figma écrit « deployements », « langages » et « Raoadmap ». La démo ci-dessus les corrige ; Figma reste à corriger.
Ce qu'il est. L'ouverture d'une page d'accueil : un visage, une salutation, la phrase qui dit ce qu'on fait, et le bouton qui mène à la suite. Un seul par page — c'est lui qui porte le h1.
Ce qu'il n'est pas. Ni un en-tête de section — ça, c'est CaseStudyHeader, qui ouvre une carte et non une page — ni un porteur de contenu : le visage, les textes et la destination du bouton tombent tous des props, parce qu'ils appartiennent au site et pas au système.
Nœud Figma 209:2348, « Section.Intro ».
Hello, I'm Judith
From user insights to business impact. Outcomes are what I design for
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
avatar | { src, alt } | — | Le visage. src passe par le withBase() du site : c’est son asset. |
greeting | string | — | La salutation, au-dessus du titre. |
headline | string | — | La phrase qui dit ce qu’on fait. |
description | string | — | La phrase sous le titre. Absente ⇒ non rendue. |
action | { label, href? } | — | La suite. Sans href ⇒ <button> inerte. |
headingLevel | 1 | 2 | 1 | Le rang du titre. 1 sur une page qui n’a pas d’autre titre. Il ne change pas le dessin : la taille vient du composant. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
L'écart sous le bloc de texte se laisse resserrer. Figma pose space.layout.64 entre le bloc et le bouton, et c'est ce que l'intro garde partout — mais la valeur est lue dans --intro-gap, jamais déclarée sur .intro, pour qu'une valeur posée sur un ancêtre passe au travers. C'est le seul endroit de l'intro où il y a de l'air à reprendre, et une page qui cale son premier écran sur la hauteur de la fenêtre en a besoin : sous 690 px, le gabarit le ramène à space.xl pour que l'intro tienne dans ce qu'on lui laisse. Déclarer la variable sur .intro battrait l'héritage et le point de surcharge ne servirait à rien.
Les tailles sont liées, les couleurs ne le sont pas. Les trois calques portent maintenant un style de texte — body/hero, heading/display, body/lead — et deux des trois tailles tombent d'une variable. Les couleurs, elles, restent tapées à la main : noir pur, #2f2d2e et #666. Les deux premières valent par chance color.text.default ; la troisième, non — le gris du système est #6a676d. Le code prend le token, donc la description ne s'affiche pas dans le gris de la maquette. À lier côté Figma, c'est le dernier morceau.
Le style heading/display se contredit. Sa description annonce « line-height.tight + letter-spacing.tight » ; ses valeurs disent 1.23 et 0. C'est la note qui a tort, pas le style : heading/title, lui, est bien serré. Le code suit les valeurs — global.css desserre le h1, et le composant ne surcharge plus rien. Deux choses restent à faire côté Figma : réécrire la description, et décider si 1.23 mérite un cran dans l'échelle. Faute de quoi le code prend line-height.snug (1.2), soit 1,3px de moins par ligne.
Le titre en mobile tient, mais pas jusqu'au plancher déclaré. La variante mobile de 142:1354 garde 42px dans un cadre de 390px, et le code la suit : « collaborative », le mot le plus long, mesure 289px et passe jusqu'à 337px de large. En dessous il sort de l'écran, alors que le fichier déclare toujours son plancher à 320px (layout/mobile). Le composant pose donc un filet sous 360px — la largeur du plus étroit des téléphones courants. Ce n'est pas le point de rupture du système (767px) : il ne change pas de mise en page, il empêche un mot de déborder. À trancher côté Figma : ou la variante mobile descend jusqu'à 320, ou le plancher remonte.
La mesure du bloc n'a pas de token. Figma la donne en dur — 929px, minimum 280px. Le composant la pose en variable locale --intro-measure, comme la carte d'étude de cas pose --header-title-min. Le jour où le système nomme ses mesures de texte, ces deux-là rentrent dans le rang.
Ce qu'il est. Les études de cas empilées, une par écran. Chaque carte est une couche collée en haut : elle monte par-dessus ce qui la précède, la suivante arrive et la recouvre, et le geste inverse défait exactement la même chose. Tout le mouvement est celui du défilement natif — aucun script ne déplace quoi que ce soit.
Ce qu'il n'est pas. Ni une grille de vignettes — la carte prend la largeur entière et n'a pas de format court — ni un porteur de contenu : les études, leurs URLs et leurs images tombent des props. Et ce n'est pas une animation : rien n'a de durée ici, le mouvement est celui du doigt.
Section Figma 729:3 (page Templates), composant 222:814, états empilés 222:912 et 222:1043.
Le premier écran de la page — nav et intro
2025 - Rubika
Faster deployments — Shared UX language — Roadmap alignment • User trust and satisfaction
2025 - Rubika
Faster deployments — Shared UX language — Roadmap alignment • User trust and satisfaction
La pile est ici en réduction, dans une boîte qui défile, et les cartes sont montrées sans leur carrousel : c'est ce qu'il faut pour tenir dans une page de doc. En vraie grandeur, une couche fait la hauteur de l'écran — voir le gabarit.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
items | { eyebrow, title, impact?, tags?, action?, images?, imagesLabel? }[] | — | Les études de cas. Ajouter une carte, c’est ajouter un objet. |
id | string | — | La cible d’ancre : c’est par elle qu’arrivent « Projets » et le bouton de l’intro. |
label | string | Études de cas | Nom accessible de la section. |
covers | string | — | L’id du bloc que la première carte recouvre — le premier écran de la page. Il devient inert quand elle est en place. |
headingLevel | 2 | 3 | 4 | 5 | 2 | Le rang des titres de carte, sous ce que la page porte déjà. Il ne change pas le dessin. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
Le premier écran — la nav et l'intro — est la couche du dessous de la pile : il tient sa place, et les cartes lui passent dessus. C'est à la page de le poser, parce que la liste ne peut pas mesurer ce qui est au-dessus d'elle, et que le mesurer en JavaScript ferait sauter la carte au chargement. Trois règles suffisent :
.home { --fold-peek: 90px; }
.home-deck { } /* le premier écran + la pile, et rien d'autre */
.home-first { position: sticky;
top: var(--nav-height);
height: calc(100svh - var(--fold-peek) - var(--nav-height)); }Une hauteur, et non un plancher : c'est elle qui garantit le liseré à toute hauteur d'écran. Avec min-height, un contenu un peu grand pousse le bloc et mange le liseré. Reste à donner à la liste l'id de ce bloc, par covers, pour qu'elle le rende inerte une fois recouvert.
.home-deck n'a aucun style et n'est pas décoratif : c'est lui qui borne le collage. Un élément positionné se peint devant un élément qui ne l'est pas, quel que soit l'ordre du DOM — sans ce bloc, le premier écran resterait collé jusqu'au bas de la page et se peindrait par-dessus tout ce qui suit la pile. Il faut donc dire où son collage cesse, et c'est là où la pile s'arrête.
Ces règles n'ont pas de variante mobile, et c'est voulu : le premier écran reste une couche à toutes les largeurs, donc le liseré est garanti partout, dès le chargement. La nav, elle, ne relève pas de ce bloc : elle porte son propre collage et son propre rang, les mêmes des deux côtés.
Une dernière règle, et elle ne se devine pas : le bloc qui suit la pile doit être positionné — position: relative suffit, sans décalage. Sur une fenêtre courte l'intro déborde de son écran par le bas ; une carte la recouvre pendant toute la pile, mais en fin de page il n'y a plus de carte. Le premier écran est collé, donc positionné, et il se peint par-dessus un bloc qui ne l'est pas, quel que soit l'ordre du DOM. Lui donner un fond n'y change rien — il serait peint dans la mauvaise couche. Positionné, il passe devant : il devient le sol qui recouvre, comme les cartes avant lui, et sans qu'un z-index soit écrit.
Le reste tombe tout seul. Le bouton de l'intro et l'onglet du Nav pointent #projets, la liste porte cette ancre, et le navigateur amène son haut à 100svh moins le liseré — c'est-à-dire exactement la position « première carte collée en haut ». Le clic et le défilement décrivent le même état sans partager une ligne de code.
| Variable | Défaut | Rôle |
|---|---|---|
--fold-peek | 90px | Le liseré de carte qui dépasse sous le bas de l’écran, et le chevauchement entre deux cartes — c’est le même nombre. À déclarer une fois sur la page. |
--layer-height | 100svh | La hauteur d’une couche. Ne se change que pour montrer la pile en réduction, comme ci-dessus. |
Le 50px de Figma n'est pas le liseré. C'est l'écart entre l'intro et la liste au repos (209:2383, itemSpacing: 50) et le chevauchement entre cartes une fois empilées (222:912, itemSpacing: −50). Le liseré, lui, tombe tout seul dans la maquette et n'y est pas constant : 166px au repos (834 − 101 − 517 − 50), 70px une fois la première carte montée. Le code fait le choix inverse — un liseré garanti dans tous les états — parce que c'est l'intention. Il vaut 90px, et ce nombre est un arbitrage, pas une lecture : il a été jugé à l'écran, et il tombe entre les 166 et les 70 que la maquette dessine. Il y était à 50 jusqu'au 2026-08-25 ; le déplacement rapproche le code de ce qui est dessiné plutôt qu'il ne l'en éloigne. Les deux lectures se rejoignent à une condition, qui est devenue la clé du calcul : une carte au repos occupe la hauteur de l'écran.
Trois mesures de la maquette ne suivent pas. Le bloc nav + intro est dessiné à 668px là où un liseré de 90 en demande 744 sur un écran de 834 — les 76px d'écart sont l'air qui manque autour de l'intro. Et les deux cartes sont dessinées à 814 et 776px là où le comportement en demande 834. À reprendre côté Figma : c'est le fichier qui doit rattraper le code, pas l'inverse.
Le mouvement n'est écrit nulle part. Aucune réaction de prototypage n'est posée sur la section Home — les trois frames sont des captures d'états, et 222:1042 mime le recouvrement en réordonnant les cartes dans son auto-layout. C'est un artifice de maquette : en défilement réel rien ne se réordonne, une couche en recouvre simplement une autre. Le comportement ne vient donc pas du fichier mais de la consigne, et c'est ici qu'il est écrit.
Le liseré n'a pas de token. Le cran voisin, space.layout.96, vaut 96 : ces 6px d'écart déplaceraient le liseré à chaque cran, puisqu'il entre dans le calcul de position. La valeur reste donc en dur, dans --fold-peek, comme --card-shadow et --intro-measure avant elle. Le jour où Figma nomme ce 90, il rentre dans le rang.
La vignette du carrousel n'est plus figée à 410 px. Une couche fait exactement un écran, et la carte est bornée à cette hauteur : l'en-tête ne bouge pas, et c'est le carrousel qui prend ou rend la place. La vignette vaut les 410px de Figma quand la fenêtre fait 735 px de haut — la hauteur naturelle de la carte — puis suit dans les deux sens : 315 à 640, 375 à 700, 509 à 834, 675 à 1000. L'image maigrit ou grossit, elle n'est jamais rognée, et rien ne sort de l'écran à aucune hauteur. C'est un écart avec Figma, qui fige cette hauteur : à trancher côté fichier.
Le seul script pose inert, et rien d'autre. Une carte recouverte garde sa place dans l'écran : elle reste donc atteignable au clavier, et le focus se poserait sur un bouton invisible — l'échec WCAG 2.4.11, Focus Not Obscured. Le cas a été mesuré, pas supposé : sans le script, le bouton « Read the story » de la première carte prend le focus alors que la deuxième le couvre entièrement. Le script constate ce que le CSS a déjà fait et le dit au clavier et aux lecteurs d'écran ; il ne déplace rien et ne dessine rien. Sans lui, la pile marche entièrement et n'est que moins sûre au clavier — le même contrat que les flèches du Carousel.
L'écart que Figma pose sous l'intro est absorbé par le centrage. Il le pose en itemSpacing sur 209:2383, mais Figma ne dessine aucun mou : son premier écran fait exactement la taille de son contenu. Dès qu'on cale ce bloc sur la hauteur de la fenêtre, il y a du mou, et le répartir autour de l'intro vaut mieux que de tout laisser tomber sous elle. Sur une fenêtre basse — moins de 690 px — c'est l'inverse : l'intro se resserre, son grand écart interne retombe à space.xl. Le liseré, lui, ne cède jamais.
La dernière carte a besoin de ce qui la suit. Le collage s'arrête au bas de la liste : sans rien après elle, la dernière couche traverse l'écran sans jamais s'y arrêter. Sur une page d'accueil, la section suivante fait cette butée — le composant n'invente pas un vide pour la remplacer.
Le premier écran est une couche, pas un décor. La nav et l'intro restent en place, et la première carte vient par-dessus elles — c'est la maquette 222:907, où la nav n'est nulle part en haut de l'écran. Elles gardent donc leur place dans la page tout en étant invisibles, ce qui les laisserait atteignables au clavier : d'où covers, et le inert que le script leur pose au bon moment. Sans cette prop, la pile fonctionne mais le bouton de l'intro reste dans l'ordre de tabulation derrière une carte opaque.
Les cartes s'empilent à toutes les largeurs, et passent sous la nav, qui écrit son propre rang et le garde des deux côtés. Sur téléphone en revanche, le premier écran n'est pas une couche : l'intro n'y tient pas, et covers n'a alors rien à rendre inerte puisque rien ne la recouvre — elle défile normalement. Voir plus bas.
Le mobile n'est pas dessiné, et la pile s'y empile quand même. Il n'y a plus de cas mobile dans ce composant — une seule règle vaut à toutes les largeurs, et le système a une bascule de moins. Ça n'a pas toujours été vrai, et l'histoire tient en deux mesures.
La pile se défaisait sous 768px parce que le contrat de la carte se retournait à cette largeur. « L'en-tête est le point fixe, le carrousel prend ou rend la place » vaut à 1200px, où l'en-tête est compact ; dans une colonne de 390 il mesurait 361px, soit 55% de l'écran, et la carte réclamait 753px. Le carrousel payait seul : sa vignette tombait à 147px sur une fenêtre de 660, à 13px sur les 553 d'un petit iPhone.
Effacer les tags a rendu 132px, et tout a basculé. L'en-tête retombe à 229px — 30 à 46% de l'écran selon l'appareil — et la carte ne réclame plus que 621px, c'est-à-dire moins que la plupart des téléphones :
Fenêtre (svh) | En-tête | Carte voulue | Carrousel | Vignette |
|---|---|---|---|---|
| 553 — petit iPhone | 257px (46%) | 649px | 176px | 144 × 310 |
| 660 | 229px (35%) | 621px | 311px | 279 × 600 |
| 760 — grand téléphone | 229px (30%) | 621px | 411px | 379 × 815 |
Les chiffres sont mesurés sur le gabarit, carte collée en haut, pas estimés. À 760 le carrousel retrouve les 411px que Figma dessine en desktop : la carte n'est plus contrainte du tout. Seul le petit iPhone serre encore, et il serre honnêtement — 144px de vignette, pas 13.
Reste un écart, et c'est le seul du système où une mesure de dessin n'est pas constante. La couche fait 100svh, c'est-à-dire la hauteur d'écran barre d'URL visible — la plus petite des deux que donne un téléphone. Quand la barre se rétracte au défilement, l'écran grandit de 60 à 90px et la carte ne bouge pas : le liseré vaut 90px au chargement et ~160px en lecture, et il repasse à 90 si l'on remonte. Aucun émulateur ne le montre — cette barre ne se rétracte que sur un appareil réel.100dvh rendrait le liseré constant, au prix d'une couche qui se redimensionne à chaque glissement de la barre : l'en-tête étant le point fixe, c'est le carrousel qui grandirait et rétrécirait de ~70px, à chaque arrêt et à chaque reprise. Une carte qui ne tremble jamais a été jugée préférable à un liseré constant. La bascule reste d'une ligne si l'usage dit le contraire.
Sur téléphone, l'intro déborde sous la première carte — et c'est le liseré qui prime. Le premier écran est une couche à toutes les largeurs : sa hauteur vaut la fenêtre moins le liseré et moins la barre, ce qui garantit l'affleurement partout. Mais l'intro de démonstration réclame 741px dans une colonne de 412, où son titre se replie sur sept lignes, quand le premier écran n'en offre qu'environ 620. Elle déborde donc par le bas, et la carte passe dessus — description et bouton compris.
Ce n'est pas un cas mobile, c'est le cas des fenêtres courtes, que le système connaît déjà : un écran de bureau trop bas produit exactement le même débordement, et justify-content: safe center le fait tomber du bon côté — l'intro reste sous la nav et ne dépasse que par le bas, là où quelque chose la recouvre toujours. Seule la fréquence change : sur téléphone c'est la règle plutôt que l'exception. L'arbitrage est explicite — le liseré prime sur ce qui dépasse — et il se renverse le jour où la maquette dessine une intro mobile plus courte. Le titre, lui, reste celui de Figma : 42px, sept lignes, et personne n'y touche pour faire tenir une géométrie.
Ce qu'elle est. Une boîte centrée par-dessus la page, avec une icône, un titre, un texte, puis une ou deux actions — une confirmation, une alerte destructive, un accusé de réception. Le ton et le nombre de boutons changent, jamais la structure.
Ce qu'elle n'est pas. Un déclencheur : rien ici n'ouvre la boîte. C'est au site consommateur d'appeler .showModal() sur son propre bouton — la modale ne connaît aucun site, donc aucun trigger ne peut vivre ici.
Figma 420:1223, un jeu de 4 variantes sous « Property 1 » — Layout (le gabarit générique, non montré ici), Confirmation, Alert, Success.
alertSeule l'icône rougit — sur color/feedback/danger/500 — et le bouton principal passe en variante alert. Le cercle, lui, reste color/bg/surface comme en défaut : voir le premier .flag plus bas.
succes, un seul boutonCette variante n'a jamais qu'un bouton — secondary est ignoré même fourni. C'est le cas Success de Figma, un accusé de réception qui ne propose rien à annuler.
Les trois modales sont montrées ouvertes (open), sans .showModal() ni script sur cette page — c'est pour ça qu'aucun fond noir ne les recouvre ici : un<dialog open> posé à la main n'entre pas dans l'état :modal du navigateur, seul .showModal() le fait. Le fond à 50 % existe bien dans le composant — voir son ::backdrop — et se voit dès qu'un site l'ouvre pour de bon.
| Prop | Type | Défaut | Rôle |
|---|---|---|---|
id | string | — | L’id du <dialog> — c’est par lui que le site consommateur appelle showModal(). |
icon | IconName | — | L’icône du cercle en tête. |
variant | 'validation' | 'alert' | 'succes' | validation | alert rougit l’icône et bascule le bouton principal en variante alert. succes n’a jamais qu’un bouton — secondary est ignoré. |
headingLevel | 2 | 3 | 4 | 5 | 6 | 2 | Le rang du titre, sous ce que la page porte déjà. Il ne change pas le dessin. |
heading | string | — | Le titre. |
body | string | — | Le texte, sous le titre. |
primary | { label, href? } | — | L’action principale. Sans href ⇒ <button> inerte. |
secondary | { label, href? } | — | L’action secondaire. Absente ⇒ un seul bouton, le cas Success. Ferme la modale sans script — c’est le seul <button type="submit"> d’un <form method="dialog">. |
class | string | — | Classe supplémentaire pour la mise en page de l’appelant. |
Le code a raison. Le cercle d'icône de la variante Alert est peint en rose dans Figma, sans qu'aucune variable ne le lie — une vraie valeur en dur côté maquette. Le code garde color/bg/surface sur les deux tons plutôt que d'inventer une couleur : c'est une décision assumée avec Judith, pas un écart à arbitrer.
L'écart entre le texte et les boutons n'a pas de token. Figma le dessine à 10px ; ni --space-sm (8) ni --space-md (12) ne tombe juste. La valeur reste en dur, dans --modal-gap, comme --card-shadow et --fold-peek avant elle.
Le fond noir n'a pas de token non plus. Aucune variable overlay ou scrim n'existe dans la collection. La valeur — rgb(0 0 0 / 0.5) — reste en dur dans --modal-scrim, en attendant que Figma la nomme.
Une coquille, reproduite telle quelle. Le corps de la modale Alert dit « Toutes les œuvres associées ne seront pas supprimer » — la maquette l'écrit ainsi (420:1229), et le mot pour mot vaut aussi pour ses fautes : à corriger côté Figma, pas dans le code.