Design system
Design system

Bac à sable

Sept ateliers. Dans chacun : les réglages à gauche, la chose en vrai au milieu, et le code en dessous — en morceaux qui se copient séparément, parce qu'ils ne vont pas au même endroit du fichier. Rien à lire avant de commencer : on touche un réglage, le code se réécrit. Les deux derniers vont plus loin : la carte pose des composants dans un composant, et le formulaire en empile une suite qu'on ordonne.

Ce que tu vois n'est pas une image, c'est la chose elle-même. Si l'aperçu est juste, le code copié donnera exactement ça. La fiche complète — props, tokens, écarts avec Figma — reste sur Composants.

Un style de texte

La règle d'abord : dans ce système, ce sont les balises HTML qui portent les styles. Un <h2> est le style « title » — il n'y a aucun CSS à écrire pour l'obtenir. Le CSS ci-dessous ne sert que lorsqu'aucune balise ne convient. Compose librement : si ta combinaison retombe sur une balise existante, l'atelier te le dira et te donnera la balise à la place.

Police

body est Inter, pour lire. heading est Poppins, pour les titres, les boutons et les marques.

Graisse

Le système met toutes ses têtes en bold, et rien de plus léger.

Interligne

tight pour les titres, snug pour ce qui tient sur une ou deux lignes, body pour ce qui se lit.

Interlettrage

Négatif sur les grandes tailles, qui ont besoin d'être resserrées. Large sur les petites capitales, qui ont besoin de respirer.

Capitales

Le texte reste écrit normalement dans ton HTML : ce sont les capitales du dessin, pas celles du contenu. Un lecteur d'écran lit toujours la phrase telle que tu l'as tapée.

Encre

Le nom dit à quoi sert le texte, pas de quoi il a l'air.

Le texte

Concevoir, c’est rendre évident ce qui ne l’était pas.

1 Dans le bloc <style>, en bas du fichier

.mon-style {
  font-family: var(--font-family-body);
  font-weight: var(--font-weight-regular);
  font-size: var(--font-size-body-lg);
  line-height: var(--font-line-height-body);
  letter-spacing: var(--font-letter-spacing-normal);
  color: var(--color-text-default);
}

Les six lignes sont écrites en entier, même celles qui ressemblent au réglage par défaut : posée sur un titre plutôt que sur un paragraphe, ta classe n'hériterait pas des mêmes valeurs.

2 Là où tu veux ce texte

<p class="mon-style">Concevoir, c’est rendre évident ce qui ne l’était pas.</p>

Button

La chose sur laquelle on clique.

Variante
Taille
Icône

Vide, le bouton n'a plus que son icône — et il lui faut alors deux précautions que le code ajoutera tout seul.

Destination

Coché, c'est un lien. Décoché, c'est un bouton à câbler.

Le composant

Voir le projetVoir le projetVoir le projetVoir le projetVoir le projetVoir le projetVoir le projetVoir le projetVoir le projet

1 Tout en haut du fichier

---
import Button from 'ux-design-system/components/Button.astro';
---

Si ton fichier a déjà un bloc ---, n'y ajoute que la ligne d'import.

2 Là où tu veux le bouton

<Button icon="mail-edit">Voir le projet</Button>

Tag

Une étiquette qui classe. Elle ne se clique pas — une pilule cliquable est un Button.

Variante

Le défaut est outline — c'est la variante que porte la carte d'étude de cas.

Le texte doit dire ce qu'il veut dire : la couleur ne porte pas de sens à elle seule.

Le composant

Recherche utilisateurRecherche utilisateurRecherche utilisateurRecherche utilisateur

1 Tout en haut du fichier

---
import Tag from 'ux-design-system/components/Tag.astro';
---

Si ton fichier a déjà un bloc ---, n'y ajoute que la ligne d'import.

2 Là où tu veux l'étiquette

<Tag>Recherche utilisateur</Tag>

Icon

Une icône est un nom, pas un chemin. Le jeu complet est surIcônes.

Taille

Sans size, l'icône prend la taille de ce qui l'entoure — un bouton lui donne la bonne.

Nom accessible

Décoché — le cas ordinaire — elle double un mot écrit à côté d'elle, donc elle est masquée aux lecteurs d'écran.

Le composant

1 Tout en haut du fichier

---
import Icon from 'ux-design-system/components/Icon.astro';
---

Si ton fichier a déjà un bloc ---, n'y ajoute que la ligne d'import.

2 Là où tu veux l'icône

<Icon name="mail-edit" size="md" />

Card

Une fiche : une image, trois lignes de texte, et trois emplacements qu'on remplit soi-même — deux rangées de tags, l'une au-dessus du titre et l'autre sous les informations secondaires, puis les actions. C'est le premier atelier où l'on met des composants dans un composant.

Orientation

Verticale, l'image est en tête. Horizontale, elle prend la moitié gauche — et sous 768px elle repasse en colonne toute seule.

Taille du titre

title est la taille d'un h2 — 2rem, ce que Figma dessine. heading est celle d'un h4 — 1.25rem, pour une grille serrée. Seule la taille bouge : le reste du dessin suit tout seul.

La seule chose que la carte exige.

Vide, la ligne n'est pas rendue : la carte ne laisse pas de trou à la place.

Image

Décochée, la carte commence au titre. L'aperçu montre une image de ce site : dans ton code, c'est src qu'il faut faire pointer sur la tienne.

Emplacement « tags-top »

La rangée qui passe au-dessus du titre, avant tout le texte. Elle est dessinée exactement comme celle du bas : seule sa place la distingue.

Emplacement « tags »

La rangée qui reste sous les informations secondaires. Figma n'accepte que des Tag dans les deux. Le code ne peut pas l'imposer — c'est à toi de ne pas y mettre autre chose.

Emplacement « actions »

Des boutons, et rien d'autre : ils se calent à droite, et leur taille sm fait 44px — c'est ce qui rend l'écart serré entre deux d'entre eux acceptable.

Le composant

Guide « Percevoir sans lutter » : la page d’ouverture et ses deux principes perceptibles.

Quatre guides pour concevoir accessible

Déploiements plus rapides, langage UX partagé

2025 — atelier de six semaines

Guide « Percevoir sans lutter » : la page d’ouverture et ses deux principes perceptibles.

Quatre guides pour concevoir accessible

Déploiements plus rapides, langage UX partagé

2025 — atelier de six semaines

Guide « Percevoir sans lutter » : la page d’ouverture et ses deux principes perceptibles.

Quatre guides pour concevoir accessible

Déploiements plus rapides, langage UX partagé

2025 — atelier de six semaines

Guide « Percevoir sans lutter » : la page d’ouverture et ses deux principes perceptibles.

Quatre guides pour concevoir accessible

Déploiements plus rapides, langage UX partagé

2025 — atelier de six semaines

1 Tout en haut du fichier

---
import Card from 'ux-design-system/components/Card.astro';
import Tag from 'ux-design-system/components/Tag.astro';
import Button from 'ux-design-system/components/Button.astro';
import { withBase } from 'ux-design-system/lib/url';
---

Il y a plusieurs lignes parce qu'il y a plusieurs composants : la carte, et ce que tu as mis dedans. Elles disparaissent d'elles-mêmes quand un emplacement se vide.

2 Là où tu veux la carte

<Card
  title="Quatre guides pour concevoir accessible"
  subtitle="Déploiements plus rapides, langage UX partagé"
  meta="2025 — atelier de six semaines"
  image={{ src: withBase('/images/projet.jpg'), alt: 'Décris ce qu’on voit', width: 822, height: 382 }}
>
  <Fragment slot="tags-top">
    <Tag>Étude de cas</Tag>
  </Fragment>

  <Fragment slot="tags">
    <Tag>Recherche utilisateur</Tag>
  </Fragment>

  <Fragment slot="actions">
    <Button variant="secondary" size="sm" iconEnd="arrow-narrow-right">Lire l’étude</Button>
  </Fragment>
</Card>

Modal

Une boîte qui s'ouvre par-dessus la page. C'est le site qui l'ouvre, jamais cette page — voir Composants pour .showModal().

Variante

alert rougit l'icône et bascule le bouton principal dans sa propre variante. succes n'a jamais qu'un bouton — c'est elle qui décide, pas le champ « Bouton secondaire » plus bas.

Vide, la modale n'a plus qu'un bouton — comme le fait toujours succes.

Le composant

Envoyer le message ?

Ta réponse partira à l’adresse du formulaire.

Envoyer le message ?

Ta réponse partira à l’adresse du formulaire.

Envoyer le message ?

Ta réponse partira à l’adresse du formulaire.

Montrée ici avec l'attribut open, sans .showModal() — c'est pour ça qu'aucun fond noir ne la recouvre. En vrai c'est ::backdrop qui le pose, gratuitement, dès qu'un site l'ouvre par script.

1 Tout en haut du fichier

---
import Modal from 'ux-design-system/components/Modal.astro';
---

Si ton fichier a déjà un bloc ---, n'y ajoute que la ligne d'import.

2 Là où tu veux la modale

<Modal
  id="ma-modale"
  icon="mail-edit"
  heading="Envoyer le message ?"
  body="Ta réponse partira à l’adresse du formulaire."
  primary={{ label: "Envoyer" }}
  secondary={{ label: "Annuler" }}
/>

Un formulaire

Le seul atelier où l'on ne règle pas un objet mais une suite : on empile des champs, on les ordonne, et le code sort en trois morceaux. Il ne fabrique pas le <form> autour — l'envoi, son bouton et la phrase des obligatoires ne sont pas des composants du système, et ils dépendent de ce que la page en fait.

Les champs

L'ordre de la liste est celui du formulaire. L'identifiant de chaque champ se déduit de son libellé — « Ton nom » donne ton-nom — et c'est lui qui lie le libellé au champ. Deux libellés identiques donneraient deux fois le même id : l'atelier le dit sous l'aperçu.

Le formulaire

1 Tout en haut du fichier

---
import TextField from 'ux-design-system/components/TextField.astro';
import Select from 'ux-design-system/components/Select.astro';
---

Une ligne par type de champ posé. Elle s'en va d'elle-même quand le dernier champ de ce type quitte la liste.

2 Là où tu veux les champs

<TextField id="ton-nom" label="Ton nom" required />

<Select
  id="sujet"
  label="Sujet"
  options={['Devis', 'Question', 'Autre']}
/>

Les champs, et rien autour : à toi d'écrire le <form>, son bouton d'envoi et la phrase « Les champs marqués * sont obligatoires » que l'astérisque oblige. Un groupe de radios ou de chips, lui, garde son <fieldset> : c'est ce qui porte le nom du groupe, et aucune prop ne le remplace.

3 Dans le style de la page

/* À poser sur ce qui entoure les champs — ton <form>, ou le <div>
   qui les groupe. */
.mes-champs {
  display: grid;
  gap: var(--space-lg);
}

/* Un groupe de radios ou de chips. Le fieldset ne sert qu'à porter le
   nom du groupe : son cadre natif s'efface, le système n'en dessine
   aucun. */
.mes-champs fieldset {
  margin: 0;
  padding: 0;
  border: none;
}

.mes-champs legend {
  margin-bottom: var(--control-field-gap);
  padding: 0;
  font-family: var(--font-family-body);
  font-size: var(--font-size-body);
  font-weight: var(--font-weight-medium);
  line-height: var(--font-line-height-tight);
  color: var(--color-text-default);
}

.mes-choix {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-lg);
}

Le système n'a pas de composant « formulaire » : la mise en page revient à l'appelant, et c'est voulu — un token partage une valeur, il ne partage pas une structure. Sans cette règle, les champs se touchent.

Les cinq lignes de la legend sont d'un autre ordre : elles redessinent à la main la typographie de FieldLabel, parce qu'aucun composant ne dessine le nom d'un groupe. Une legend n'a pas de for — ce n'est pas un libellé de champ, et FieldLabel ne peut pas la rendre. C'est un manque du système, pas un choix.

Quand ça ne marche pas

Cinq pannes, dans l'ordre où on les rencontre. Chacune a un signe et un geste.

La page est blanche, ou le composant s'affiche en toutes lettres

Le signe. Tu vois <Button> écrit tel quel dans la page, ou une erreur qui dit qu'il n'est pas défini.

Le geste. La ligne d'import manque. Elle va tout en haut du fichier,entre les deux lignes de trois tirets — ce bloc s'appelle le frontmatter, et c'est le seul endroit où un import a le droit d'être.

Tout est là mais rien n'est stylé

Le signe. Les textes s'empilent en Times New Roman, le bouton n'est qu'un rectangle gris.

Le geste. Les feuilles du système ne sont pas chargées. Si ta page passe par BaseLayout, elles arrivent toutes seules. Sinon, une fois pour tout le site :

import 'ux-design-system/styles/tokens.css';
import 'ux-design-system/styles/global.css';

Dans cet ordre : le second se sert des variables du premier.

« Icon : “xxx” n'existe pas »

Le signe. Le build s'arrête, et l'erreur liste toutes les icônes disponibles.

Le geste. Le nom n'est pas dans le registre — souvent une faute de frappe ou un tiret oublié. Recopie-le depuis Icônes, ou depuis le menu déroulant de l'atelier ci-dessus. Pour en ajouter une, c'est un export Figma déposé dans icons/ puis npm run icons.

Une prop que tu écris ne fait rien

Le signe. Aucune erreur, aucun effet. C'est la panne la plus coûteuse, parce qu'elle est muette.

Le geste. npm run build ne vérifie pas les noms de props. C'est npm run check qui le fait, et qui te dira laquelle n'existe pas :

npm run check

À lancer avant de pousser, systématiquement.

Ce que tu mets dans une carte n'apparaît pas

Le signe. Aucune erreur, et les tags — ou les boutons — ont simplement disparu. La carte s'affiche, elle est juste vide à cet endroit.

Le geste. Vérifie que le contenu est bien enveloppé dans son Fragment. La carte a deux emplacements nommés : ce qui n'annonce pas son nom n'atterrit nulle part, et Astro le laisse tomber sans rien dire.

<!-- ne s’affiche pas : le Tag ne dit pas où il va -->
<Card title="…">
  <Tag>Recherche utilisateur</Tag>
</Card>

<!-- s’affiche -->
<Card title="…">
  <Fragment slot="tags">
    <Tag>Recherche utilisateur</Tag>
  </Fragment>
</Card>

Le code de l'atelier Card l'écrit toujours correctement : en cas de doute, recopie-le plutôt que de l'écrire de mémoire.

Une image marche en local et donne un 404 en ligne

Le signe. Rien à voir en développement, et l'image manque une fois le site publié.

Le geste. Le site est publié sous un préfixe. Tout chemin absolu — image, lien vers une page — passe par withBase() :

---
import { withBase } from 'ux-design-system/lib/url';
---

<img src={withBase('/images/avatar.png')} alt="" />

Les icônes échappent à cette règle : inlinées, elles n'ont pas d'URL.