Design system

Palette, accent, typographie, espace, formes, composants et thème sombre, la référence visuelle du produit.

Référence visuelle du produit. Décrit l'apparence, le ressenti et les règles visuelles, indépendamment de la technologie qui les implémente.

Philosophie

Fraza est un outil de travail, pas un produit marketing. L'interface s'efface derrière le contenu et derrière la marque du client.

Un seul principe directeur : ne jamais ajouter un élément sans raison. Chaque couleur, bordure, espace ou texte supplémentaire doit justifier sa présence. Le doute se résout toujours en faveur de la suppression.

Cinq règles en découlent :

  1. Sobre. Noir, blanc, gris, et une seule couleur d'accent.
  2. Personnalisable. Le projet change l'accent et le logo, et le reste suit. Aucune couleur n'est écrite en dur dans un composant.
  3. L'accent est rare. Il guide l'œil vers ce qui est interactif ou important. S'il est partout, il ne guide plus rien.
  4. Lisible d'abord. Contrastes suffisants, tailles de texte confortables, états clairs : survol, focus, erreur, désactivé.
  5. Pas d'effet décoratif. Pas de dégradés, pas de barres obliques, pas d'ombres sur les cartes. La structure vient des bordures et des espaces.

Palette

Base

Par défaut, le produit est noir, blanc et gris, avec un seul accent, le bleu fleuve #2457c5. Le fond de la zone de contenu est un gris très pâle, les cartes sont blanches, le texte presque noir, les boutons principaux à l'encre.

Un projet peut poser sa marque : un accent, une teinte de gris, une barre foncée, un bouton principal coloré. Elle passe par le thème du projet, jamais par une couleur écrite dans un composant. Les règles de sobriété qui suivent valent quel que soit le thème.

Jetons

Un composant n'écrit jamais une nuance de palette, seulement un rôle. text-fg-muted, pas text-neutral-500 dark:text-neutral-400. Le jeton porte à la fois le clair, le sombre et la marque du projet : une nuance écrite en dur ignore l'accent du projet, et oublie le sombre une fois sur deux.

RôleJetonClairSombreUsage
Fondscanvas#f8f8f8#000000Fond de la zone de contenu de l'admin.
surface#ffffff#0b0b0bCartes, menus, fenêtres, sidebar.
surface-muted#f5f5f5#141414Puces, conteneur d'un sélecteur segmenté, fonds gris.
surface-hover#fafafa#141414Survol d'une ligne ou d'une carte, en-tête de tableau en clair.
Textefg#171717#fafafaTitres et corps de texte.
fg-secondary#525252#d4d4d4Éléments du menu, libellés de champs.
fg-muted#737373#a3a3a3Textes d'aide, métadonnées, en-têtes de colonnes.
fg-subtle#a3a3a3#737373Indices, textes d'exemple, groupes du menu, texte désactivé.
Lignesline#e5e5e5#1f1f1fBordures des cartes, séparateurs.
line-strong#d4d4d4#333333Bordures appuyées, interrupteur éteint.
line-soft#f0f0f0#171717Séparateurs des lignes de tableau, fond d'un bouton désactivé.
Champsfield#ffffff#141414Fond des champs de saisie et des cases non cochées.
Contrôlescontrol#ffffff#141414Fond d'un bouton secondaire.
control-hover#f5f5f5#1f1f1fSon survol.
control-line#d4d4d4#333333Bordure des boutons secondaires et des champs.
Bouton principalprimary#0a0a0a#fafafaBouton principal, page courante d'une pagination, onglet actif.
primary-hover#2e2e2e#e5e5e5Son survol.
primary-fg#ffffff#0a0a0aTexte posé dessus.
Accentaccent#2457c5#7aa5f8Liens, focus, cases cochées, interrupteur activé, élément actif, graphiques.
accent-hover#1d4aa8#a3c1faSurvol d'un lien ou d'un élément accentué.
accent-fg#ffffff#0a0a0aTexte posé sur l'accent plein.
accent-soft8 % d'accent14 % d'accentFond de l'élément actif du menu, pastille « Envoyée », alerte d'information.
accent-line35 % d'accent35 % d'accentBordure accentuée, cible de dépôt.
Focusring= accent= accentAnneau de focus.
Barre et sidebarchrome, chrome-fg, chrome-fg-muted, chrome-line, chrome-hoversurface, fg, fg-secondary, line, surface-mutedidem, avec fg-muted pour chrome-fg-mutedCoquille de l'admin uniquement.
Statutsdanger, success, warning, infovoir ci-dessousChacun avec -soft, -line, -ink, -fg.
danger-hover#b91c1c#ef4444Texte d'une action destructrice discrète.
danger-solid#dc2626#dc2626Fond du bouton danger plein, sous un texte danger-fg blanc dans les deux modes.

accent-soft vaut environ #edf2fb en clair. Les déclinaisons de l'accent et de l'info sont calculées par color-mix() à partir de la couleur pleine : elles suivent un accent changé sans rien écrire de plus. Celles de danger, success et warning sont des teintes fixes, décrites dans Statuts.

Les jetons control et field existent parce qu'un contrôle ne se comporte pas pareil dans les deux modes. En clair, il est blanc et sa bordure le dessine. En sombre, une bordure seule ne le détache pas d'une carte presque noire : il prend un fond gris plein, #141414. Aucun jeton de surface n'a ce comportement, d'où ces rôles à part.

primary, accent et fg sont trois choses différentes. fg est l'encre du texte. primary est le bouton principal, à l'encre par défaut. accent est la couleur qui guide : un lien, un focus, l'élément actif. Un titre en accent passerait à la couleur du projet, un lien en fg ne se verrait plus comme un lien.

Le garde-fou npm run check:colors fait échouer le build du core dès qu'une nuance de palette, une couleur dark: ou une couleur arbitraire [#…] apparaît. Un module lance le même script sur son code : node ../core/scripts/check-colors.js dist. Les rares exceptions (couleurs catégorielles, couleurs choisies par l'utilisateur) sont déclarées une par une dans le script, avec leur raison.

L'accent

L'accent est le seul endroit où la couleur entre dans l'interface. Par défaut c'est le bleu fleuve, #2457c5, à environ 6:1 sur blanc : lisible même en petit texte. En sombre, il s'éclaircit en #7aa5f8.

L'accent sert à :

  • les liens, comme « Voir tout → », en accent sans soulignement au repos, soulignés au survol ;
  • l'anneau de focus et la bordure du champ actif ;
  • les cases cochées, les boutons radio et les interrupteurs activés ;
  • l'élément actif du menu : fond accent-soft, icône en accent, texte en graisse 600 ;
  • les graphiques, le statut « info » et le statut « Envoyée », les compteurs « Nouveau ».

L'accent ne sert pas à : les boutons principaux, qui restent à l'encre ; les titres ; les grandes surfaces.

Un projet change l'accent avec l'option accent du thème. Les variantes, survol, fond pâle et version sombre, sont calculées à partir de lui. Un accent doit garder un contraste d'au moins 4,5:1 sur blanc. Un jaune pâle ou un vert clair ne passe pas : le core l'assombrit au démarrage et affiche un avertissement qui nomme la valeur donnée, son contraste et la valeur retenue. Voir Personnaliser l'apparence.

Statuts

Quatre statuts, utilisés avec parcimonie : pour les états, les alertes, les variations. Jamais comme couleur décorative, jamais en fond de section.

JetonClairSombreUsage
success#15803d#4ade80Facture payée, confirmation, actif, variation positive.
warning#a16207#facc15Paiement partiel, échéance proche, brouillon.
danger#dc2626#f87171Erreur, champ invalide, suppression, variation négative.
info= accent= accentInformation, mise à jour disponible, statut « Envoyée ».

La couleur pleine sert au point d'une pastille, au fond d'un bouton plein, à un chiffre qui demande l'attention. Autour d'elle, chaque statut a trois déclinaisons, qui forment toujours un ensemble :

DéclinaisonRôledangersuccesswarning
-softFond pâle : pastille, alerte, carte de statut.#fef2f2#f0fdf4#fffbeb
-lineBordure d'une alerte, du bouton dangerSoft.#fecaca#bbf7d0#fde68a
-inkTexte posé sur le fond pâle.#991b1b#166534#92400e

Ce sont les teintes de la charte, pas des mélanges : un mélange de la couleur pleine donnait des fonds grisés et un texte sous 4,5:1. En clair, l'encre est plus foncée que la couleur pleine, « Payée » en #166534 sur #f0fdf4. En sombre, les fonds sont des voiles de la couleur, 14 à 15 % pour -soft et 30 % pour -line, et l'encre est la couleur elle-même.

L'info suit l'accent. Son fond pâle est celui de l'accent, 8 % en clair et 16 % en sombre, sa bordure 20 % et 30 %, et son encre est l'accent.

danger-hover est le rouge foncé de la charte. Il sert au texte d'une action destructrice discrète : le bouton dangerSoft, l'item « Supprimer » d'un menu. En sombre, ces textes gardent danger, plus lisible sur le noir. Le survol du bouton danger plein assombrit son fond de 15 %.

Le bouton danger plein ne prend pas danger mais danger-solid : le rouge franc de la charte, #dc2626, dans les deux modes, sous un texte blanc. Le rouge éclairci du sombre, #f87171, est fait pour se lire en texte sur le noir. Posé en fond, il demandait un texte noir et le bouton passait pour une alerte saumon. Les textes d'erreur, les points de pastille et les barres restent sur danger.

On ne mélange pas les déclinaisons de statuts différents. Sur la couleur pleine, le texte prend -fg : blanc en clair, #0a0a0a en sombre.

Les couleurs de statut ne changent pas avec le thème du projet : le rouge veut dire erreur partout, pour tout le monde. Seul info suit l'accent. Un projet qui change malgré tout un statut par overrides reçoit des déclinaisons calculées, voir Thèmes.

Données et catégories

Deux sortes de couleurs échappent aux jetons, et c'est voulu. Une couleur choisie par l'utilisateur est une donnée : la couleur d'un rôle, d'un service de réservation, un texte coloré dans l'éditeur. Elle s'affiche telle qu'elle a été saisie, en style inline. Une couleur catégorielle distingue des types entre eux, pas un état : les icônes par type de fichier de Drive, par exemple. La lier à l'accent ferait se confondre deux catégories. Une pastille, elle, n'a pas de couleur catégorielle : les variantes purple, pink et orange de Badge sont rendues en neutre, parce que la couleur d'une pastille dit un état.

Ce qu'on n'utilise pas

  • Dégradés.
  • Barres ou éléments décoratifs en oblique.
  • Couleurs de fond dans les sections, sauf survol et état actif.
  • Une couleur de marque écrite en dur dans un composant.
  • Transparence et flou, sauf le voile des fenêtres et les fonds de statut du mode sombre, qui sont des voiles de leur couleur.

Le logo Fraza existe en trois variantes : complet (icône et mot), texte seul, icône seule. Il est noir #0a0a0a sur fond clair, blanc #fafafa sur fond foncé, jamais en couleur, ni à l'accent. On ne le déforme pas, on ne l'incline pas, on n'y ajoute ni ombre ni contour.

Dans l'application, il apparaît à un seul endroit :

  • Pages d'authentification : logo centré au-dessus de la carte, hors d'elle, à 24 px de son bord. C'est le logo du projet quand FRAZA_LOGO_URL est posée, avec FRAZA_LOGO_URL_DARK en sombre. Sinon, le logo Fraza complet à 26 px de haut.

Le composant FrazaLogo dessine le logo en SVG, dans la couleur du texte. Voir Composants.

Typographie

Police unique : IBM Plex Sans. Pour les données qu'on compare ou qu'on recopie, numéros de facture, heures, dates techniques, codes : IBM Plex Mono. Un projet peut changer ces deux familles avec l'option font du thème : les composants écrivent font-ui et font-code, jamais un nom de police.

RôleTailleGraisseNotes
Titre de page24 px700Interlettrage −0,03 em.
Titre de carte15 px600
Titre de fenêtre16 px700
Titre des pages d'authentification20 px700Interlettrage −0,02 em.
Corps de texte13 à 14 px400
Menu (élément inactif)13,5 px400Sous-item de la sidebar : 13 px.
Menu (élément actif), boutons13 à 13,5 px500 à 600
Libellés de champs12,5 px500
Petit texte12 px400Textes d'aide, en-têtes de tableau.
Pastilles de statut12 px500
Groupes du menu11,5 px500fg-subtle.
Données comparées ou recopiées12,5 à 13 px400IBM Plex Mono.
Chiffre d'une carte de statistique24 px700Interlettrage −0,03 em.

Taille minimale : 11 px. En dessous, on ne descend pas, sauf le compteur numérique d'un item de menu.

Règles d'écriture

  • Sentence case partout. « Tableau de bord », pas « TABLEAU DE BORD ». Les en-têtes de colonnes suivent la même règle.
  • Texte court et direct. Pas de phrase là où un mot suffit.
  • La chasse fixe est réservée à ce qu'on compare ou qu'on recopie : numéros de facture, codes, identifiants, dates techniques. Les noms, les libellés et les dates relatives, « il y a 8 minutes », restent en IBM Plex Sans. Un montant dans un tableau peut rester en sans, avec des chiffres tabulaires (tabular-nums) pour s'aligner.

Voir Rédaction pour la voix des textes.

Icônes

Un seul jeu : Hugeicons, jeu libre, style « stroke rounded ». Un trait arrondi de 1,8, sans remplissage. Le core ne recopie que les icônes de sa propre interface, et chaque module recopie les siennes dans son code, sous licence MIT : la licence accompagne chaque paquet. Jamais d'icône Hugeicons Pro dans un paquet publié : sa licence interdit de la redistribuer. Un projet peut puiser ailleurs, avec un trait de 1,8 pour rester dans la charte.

  • 16 px dans la navigation, les boutons et les menus, 14 px dans un bouton sm.
  • Couleur héritée du texte (currentColor). L'élément actif colore son icône en accent ; on ne passe pas à une variante pleine.
  • Une icône a une fonction. Pas d'icône décorative, pas de tuile colorée derrière une icône.
  • On ne dessine pas une icône à la main dans un composant.

Voir Icônes pour la provenance, le jeu du core et les icônes d'un module ou d'un projet.

Espace

Base : 4 px. Les espacements entre blocs suivent les multiples de 4 du tableau ci-dessous. À l'intérieur d'un composant, la charte règle plus finement, le plus souvent sur une grille de 2 px : 10 px de gouttière d'un item de menu, 14 px d'un bouton, 18 px d'une tuile de statistique. Ces réglages vivent dans les recettes des composants, pas dans les mises en page.

ValeurUsage
4 pxEspacement interne minimal, entre icône et texte
8 pxPadding compact, écart entre éléments proches
12 pxPadding interne des petits composants
16 pxPadding standard des cartes et panneaux, écart entre les cartes
24 pxEspacement entre sections
28 pxPadding du contenu principal de l'admin
48 à 64 pxEspacement entre blocs majeurs

La densité est moyenne : ni étouffante, ni aérée. Un admin s'utilise plusieurs heures par jour, la densité confortable prime sur l'esthétique aérée.

Formes et bordures

Rayons

ContexteRayon
ContexteRayon
------
Items de menu, étiquettes, infobulles, segments d'un sélecteur, petits boutons icône6 px
Boutons à toutes les tailles, champs, cases de pagination, piste d'un sélecteur segmenté8 px
Cartes, tableaux, modales, panneaux, menus déroulants, toasts, alertes12 px
Pastilles de statut, interrupteurs, avatarsComplet

Trois rayons, plus le rond. On ne mélange pas les rayons dans un même composant, et on n'écrit pas de rayon en dur : l'option radius du thème fait varier les trois jetons ensemble.

Une seule exception, à 4 px : les éléments de 18 px ou moins, où 6 px ferait une pastille. Ce sont les cases à cocher, le bouton × d'une étiquette, une touche <kbd>, les barres d'un squelette, et l'anneau de focus d'un lien texte.

Hauteurs

ÉlémentHauteur
Bouton ou champ sm30 px
Bouton ou champ md, le défaut36 px
Bouton ou champ lg42 px
Item de la sidebar34 px
Sous-item de la sidebar30 px
Bouton du pied d'une fenêtre34 px
Bouton principal des pages d'authentification40 px
Item d'un menu déroulant32 px
Case de pagination, bouton d'action d'une ligne de tableau30 px
Barre supérieure, barre d'une page publique60 px

Bordures et profondeur

Toutes les séparations sont des bordures de 1 px solid. Les cartes ont une bordure, jamais d'ombre. Pas de ligne de 2 px ou plus, sauf le trait de l'onglet actif et le repère du sous-item actif de la sidebar.

Les ombres existent uniquement pour ce qui se superpose à l'interface. Elles restent discrètes, juste assez pour indiquer la profondeur :

ÉlémentOmbre
Menu déroulant, tiroir mobile0 8px 24px à 12 %, 60 % en sombre
Toast0 8px 24px à 15 %
Fenêtre0 16px 48px à 20 %

En sombre, où une ombre ne se voit plus sur le noir, la bordure d'un menu ou d'une fenêtre monte à line-strong : c'est elle qui détache le panneau.

Focus clavier

Un anneau de 2 px en accent (ring), décalé de 2 px de l'élément : focus-visible:ring-2 ring-ring ring-offset-2, avec un ring-offset de la couleur du fond. Toujours visible au clavier, jamais retiré. Un champ de saisie prend à la place une bordure en accent et un halo accent de 3 px, à 18 % en clair et 22 % en sombre. core.css pose le même anneau en outline sur tout :focus-visible : un lien ou un bouton sans recette de focus en a quand même un.

Structure de l'admin

┌──────────────────┬──────────────────────────────────┐
│ Logo du projet   │ Barre supérieure 60px            │
│──────────────────├──────────────────────────────────│
│ Tableau de bord  │                                  │
│ Pages            │   Zone de contenu                │
│                  │   padding 28px, fond canvas      │
│ Modules          │                                  │
│ Formulaires      │   cartes blanches bordées        │
│                  │                                  │
│ Administration   │                                  │
│ Utilisateurs     │                                  │
│                  │                                  │
└──────────────────┴──────────────────────────────────┘
  Sidebar 232px
  • Sidebar : fixe, 232 px, fond chrome, séparée par une bordure droite.
  • Barre supérieure : fixe, 60 px, sur le fond du contenu (canvas). Fil d'Ariane à gauche, sans icône, séparé par des « / » : parents en chrome-fg-muted, page courante en chrome-fg graisse 500. À droite, les emplacements des modules puis le menu utilisateur : avatar de 30 px et nom en 13 px graisse 500, sans chevron. Le survol d'un bouton de la barre prend line-soft.
  • Contenu : canvas, gris très pâle, padding 28 px. Les cartes et tableaux sont en surface, ce qui crée la séparation visuelle sans bordure de section. En sombre, le fond devient noir et les cartes #0b0b0b.

La sidebar porte les jetons chrome-*, distincts des surfaces. Par défaut, elle prend la surface dans les deux modes : blanche en clair, #0b0b0b en sombre, séparée du contenu par sa bordure. Avec chrome: 'dark', elle reste foncée quel que soit le mode, et le contenu garde ses surfaces claires. La barre du haut, elle, reprend toujours les couleurs du contenu : la classe fraza-topbar-canvas fait pointer ses jetons chrome-* sur canvas, fg, fg-muted, line et line-soft.

Coquille topbar

Avec shell: 'topbar', la navigation quitte la sidebar pour la barre du haut, de 60 px, sur le fond chrome. De gauche à droite : la marque, les sections de navigation, puis les emplacements des modules et le menu utilisateur. Le contenu prend toute la largeur.

┌───────────────────────────────────────────────────────────┐
│ Logo  Tableau de bord  Utilisateurs ▾  Projets    🔔  (JT) │
├───────────────────────────────────────────────────────────┤
│                                                           │
│   Zone de contenu, padding 28px                           │
│                                                           │
└───────────────────────────────────────────────────────────┘

Une section à un seul item est un lien, une section à plusieurs items un menu déroulant : c'est la règle de l'accordéon de la sidebar, posée à l'horizontale. L'item actif prend le fond accent-soft. Sous 1024 px, le bouton de menu ouvre le tiroir de la sidebar. Il n'y a pas de fil d'Ariane : le titre de la page, dans AdminHeader, suffit à situer l'utilisateur quand la navigation est visible en permanence.

Cette coquille convient à une application aux sections peu nombreuses, un portail client par exemple. Au-delà de six ou sept sections, la barre déborde : la sidebar reste le bon choix.

En-tête de page

AdminHeader sépare deux zones : à gauche ce que la page est, à droite ce qu'on fait avec.

  • À gauche : le titre, en 24 px graisse 700, interlettrage −0,03 em, le badge d'état à 10 px du titre, la description en 13 px fg-muted, 4 px dessous. Le badge se passe en prop badge et se pose à côté du titre. Un état n'est pas un contrôle : glissé dans le groupe d'actions, il casse l'alignement de la rangée et se lit comme un bouton mort.
  • À droite : le bouton retour puis les actions, toutes à la taille par défaut md. Une seule taille par rangée. Mélanger sm et md côte à côte donne deux hauteurs de boutons et c'est ce qui se voit en premier.

Trois contrôles maximum à droite, bouton retour compris : une action destructive, une action principale. Au-delà, les actions se regroupent dans un bouton split. Un bouton icône seule n'a pas sa place dans un en-tête : sans libellé à côté de voisins qui en ont, il ne se lit pas. Sa place est dans une ligne de tableau ou dans le menu du bouton split.

Pages publiques

Les pages servies aux visiteurs hors de l'admin, partage de fichiers, prise de rendez-vous, facture à payer, formulaire, passent toutes par le même cadre : PublicShell, exposé par @fraza/core/shared/public. Une personne qui reçoit deux liens du même site ne doit pas avoir l'impression de changer de site en cours de route.

Deux dispositions. Un état qui tient en une phrase et au plus un champ, lien expiré, mot de passe demandé, lien fermé, prend une boîte centrée (layout="centered"). Une vraie interface prend la page (layout="page").

La boîte centrée est la carte de la page de connexion : 380 px, gouttière de 32 px, logo de 26 px au-dessus de la carte, à 24 px d'elle, titre de 20 px graisse 700, phrase de 13 px en fg-muted. Un message d'état s'y pose en alerte : fond -soft, bordure -line, texte -ink du statut, icône à gauche. Jamais une carte colorée dans la carte.

Deux silhouettes d'en-tête

comfortablebar
Logo64 px, au-dessus du titre24 px, à gauche dans la barre
Titre28 à 32 px, 700, interlettrage −0,035 em14 px, 600, centré dans la barre
Positiondéfile avec la pagecollant en haut, 60 px, fond canvas, bordure basse, sans flou
Respiration au-dessus du contenu64 à 96 px32 à 48 px

La règle : une page qu'on ouvre pour regarder prend comfortable, une page qu'on ouvre pour faire quelque chose prend bar. Un partage de fichiers et une facture sont des documents : le grand titre les annonce comme une page d'accueil annonce un site. Un assistant de réservation et un formulaire sont des outils : l'en-tête y est de la hauteur prise à la tâche, et un visiteur qui a déjà lu le titre une fois n'a pas besoin qu'il occupe le tiers de son écran.

En silhouette bar, le titre de la barre est le titre de la page : il n'y a pas de second titre en dessous. Un sous-titre, s'il y en a un, devient la première ligne de la colonne de contenu.

Une page en silhouette bar qui déplace elle même le défilement doit poser une marge de défilement d'au moins 80 px (scroll-mt-20) sur sa cible, sans quoi celle ci se gare sous la barre.

Marque et pied de page

Le logo vient de la configuration du site, décliné par thème. Sans logo configuré, rien n'est rendu : un nom posé là où l'œil attend une image se lit comme un logo qui n'a pas chargé. Le nom du site reste dans le pied de page, en 12 px fg-muted, où il est à sa place, suivi des mentions propres au module : lien privé, numéro de facture, numéros de taxes.

La bascule de thème flotte dans le coin bas de l'écran. Toute page publique garde une réserve en bas de page, pour que la fin du contenu ne reste pas cachée sous le bouton.

Trois zones verticales :

  1. Logo ou nom du projet, hauteur 60 px, aligné sur la barre supérieure, séparé par une bordure basse.
  2. Navigation principale, défilante si nécessaire, 8 px de chaque côté des items, 12 px en haut et en bas, 2 px entre deux items.
  3. Pied, séparé par une bordure haute, jamais dans la zone défilante : les items épinglés. Sans item épinglé, le pied n'est pas rendu.

Groupes

La navigation se range en quatre blocs, dans cet ordre :

  1. Les sections sans groupe, sans libellé : le tableau de bord.
  2. Le groupe « Contenu » : médias, pages, articles.
  3. Le groupe « Outils » : formulaires, rendez-vous, facturation, fichiers.
  4. Le groupe « Administration » : utilisateurs, devkit.

Les groupes classent par usage, pas par provenance : un module ne forme pas un groupe à lui seul, il se range dans celui qui décrit ce que l'utilisateur y fait. La liste est fermée pour que la sidebar garde la même forme, quel que soit le nombre de modules installés. Un groupe sans section n'est pas rendu.

Chaque groupe porte un libellé en 11,5 px graisse 500, fg-subtle, sans majuscules. Une section choisit son groupe à l'enregistrement, avec le champ group. Voir Étendre l'admin.

Deux types d'items

  • Lien direct : une section qui ne contient qu'un item s'affiche comme un lien simple portant le titre de la section, sans chevron. Un post type « Actualités » sans taxonomie se résume donc à un bouton « Actualités ».
  • Section accordéon : une section à plusieurs items. Un bouton parent déploie ou replie les sous-items.

États

ÉtatApparence
InactifTexte chrome-fg-muted, graisse 400. Fond transparent.
SurvolFond chrome-hover. Texte chrome-fg.
ActifFond accent-soft. Texte chrome-fg, graisse 600. Icône en accent.
Parent de l'item actifMême apparence qu'un item actif, que la section soit ouverte ou repliée.
Sous-item actifPas de fond. Texte chrome-fg, graisse 600. Repère de 2 × 16 px en accent, posé sur la ligne guide.

Gabarit d'un item

Hauteur 34 px, padding horizontal 10 px, rayon 8 px, icône 16 × 16 px à gauche avec un écart de 10 px, texte 13,5 px, transition 120 ms ease-out. Un compteur optionnel se place à droite : pastille ronde de 18 px en accent, texte accent-fg en 10,5 px graisse 600. C'est un compteur de nouveautés : le rouge reste réservé aux erreurs.

Les sous-items sont plus petits que les sections : hauteur 30 px, padding horizontal 9 px, rayon 6 px (rounded-item), texte 13 px, sans icône. Ils pendent d'une ligne guide de 1 px en chrome-line, placée sous le centre de l'icône de la section. Leur texte s'aligne sur le titre de la section. Le fond accent reste sur la section : le sous-item actif se distingue par sa graisse et par son repère sur la ligne guide.

Accordéon

Le chevron de 14 × 14 px est à l'extrême droite du bouton parent. Il pivote de -90deg quand la section est repliée. L'animation du conteneur combine max-height et opacity, sur 120 ms ease-out.

Une section s'ouvre automatiquement au chargement si elle contient l'item actif. Toutes les autres sont repliées.

Mobile

Sous 1024 px, la sidebar sort du flux et se positionne en fixe sur toute la hauteur. Masquée par défaut, elle s'ouvre par le bouton de menu de la barre, avec un voile noir à 42 % et l'ombre d'un panneau superposé. Ouverte, elle porte sa propre croix de fermeture, puisqu'elle couvre le bouton de menu. Cliquer le voile ou naviguer la ferme. Au-delà de 1024 px, elle se referme automatiquement.

Boutons

VarianteApparenceUsage
PrimaireFond primary (encre), texte primary-fg. Survol primary-hover.L'action principale de l'écran. Un seul par zone.
SecondaireFond control (blanc), bordure control-line, texte fg. Survol control-hover.Actions secondaires : Annuler, Exporter.
FantômeSans fond ni bordure, texte fg-secondary. Survol : fond line-soft (control-hover en sombre), texte fg.Actions discrètes : Aperçu, icônes dans un tableau.
Lien (link)Texte en accent, sans hauteur ni gouttière. Souligné au survol seulement.Lien d'action : « Voir tout → », « Voir le détail → ».
DangerFond danger-solid (rouge franc dans les deux modes), texte danger-fg blanc. Survol : le fond assombri de 15 %.Suppression, action destructive.
dangerSoftFond surface, bordure danger-line, texte danger-hover (danger en sombre). Survol danger-soft.Le danger discret : suppression dans une liste ou un contexte secondaire.

Un lien d'action n'est pas un bouton plein : il prend la variante link, ou un lien en text-accent, sans soulignement au repos.

Le bouton principal reste à l'encre même quand le projet change d'accent. Un projet peut lui donner une couleur avec l'option primary du thème : le texte posé dessus passe alors au blanc ou au noir selon le contraste.

Six variantes, celles de la charte, et pas une de plus. Un succès ou un avertissement n'est pas une action : il se dit par une pastille, une alerte ou une notification, jamais par un bouton vert ou jaune.

Les variantes pleines sont la règle pour les actions principales : un bouton doit dominer visuellement la pastille à côté. dangerSoft sert quand la suppression est répétée, par exemple une icône de suppression sur chaque ligne d'un tableau, ou quand le bouton est secondaire à côté d'un autre déjà plein. Les pastilles de statut gardent leur traitement propre : ne pas les confondre avec ces variantes.

Il ne doit jamais y avoir plus d'un bouton primaire par section ou en-tête de page.

Tailles

TailleHauteurPadding horizontalTexte
sm30 px12 px12,5 px
md36 px14 px13 px
lg42 px18 px14 px

La taille par défaut est md. Un bouton icône seule est carré, de la même hauteur. Dans le pied d'une fenêtre, les boutons md passent en 34 px sur 12 px de gouttière, la version compacte de la charte.

Règles communes

Rayon 8 px à toutes les tailles (rounded-control), graisse 500, transition 120 ms ease-out, anneau de focus en accent. Une icône de 16 px (14 px en sm) peut se placer à gauche ou à droite du texte. Un bouton icône seule a toujours un libellé accessible (aria-label).

L'état désactivé dépend de la famille, curseur interdit dans tous les cas. Un bouton plein prend un fond line-soft et un texte fg-subtle. Un bouton bordé garde son fond, sa bordure pâlit et son texte passe en fg-subtle. Un bouton fantôme pâlit encore, sans fond.

L'état de chargement bloque le clic mais garde les couleurs de la variante : un bouton qui travaille n'est pas un bouton désactivé. L'indicateur prend la place de l'icône, ou se pose au centre du libellé, sans changer la largeur du bouton.

Bouton split

Quand plusieurs actions partagent la même intention avec des intensités différentes (Déployer, Démarrer sans rebuild, Redéployer sans cache), on n'aligne pas trois boutons primaires : visuellement ils se ressemblent et l'utilisateur ne sait plus lequel choisir. On utilise SplitButton : un bouton principal à gauche, un chevron à droite qui ouvre un menu de variantes. Chaque item peut porter une description qui explicite la nuance. Son panneau est ancré dans un portail, donc il reste lisible dans une modale ou un tableau défilant.

Une action d'intention opposée (Arrêter face à Démarrer) ne va pas dans le menu. Elle reste un bouton séparé, généralement en variante danger. Le SplitButton regroupe des variantes du même verbe, pas son inverse.

C'est aussi la sortie quand un en-tête déborde : plusieurs façons de remettre la même ressource au client (Renvoyer, Copier le lien, Télécharger le PDF) tiennent dans un seul bouton au lieu d'occuper trois places. Il accepte une icon comme Button, ce qui lui permet de remplacer une action principale sans que la rangée change d'allure.

Quand l'action principale disparaît selon l'état de la ressource, c'est l'item suivant du menu qui prend sa place. On ne laisse pas un chevron au-dessus d'un bouton mort.

Champs

  • Fond field, bordure control-line, rayon 8 px, hauteur 36 px, texte de 13 px, gouttière de 12 px. Libellé au-dessus en 12,5 px graisse 500, couleur fg, texte d'aide en 12 px fg-muted sous le champ.
  • Focus : bordure en accent et halo accent de 3 px, à 18 % en clair et 22 % en sombre.
  • Erreur : bordure rouge et halo rouge de 3 px à 12 %, message en rouge sous le champ.
  • Désactivé : fond canvas, bordure line, texte fg-subtle, libellé et aide grisés avec lui. Le champ ne s'estompe pas en entier.
  • Lecture seule : la même boîte grise, la valeur reste lisible.
  • Unité : une zone canvas à droite du champ, séparée par un filet line, porte l'unité, « $ CA » par exemple (suffix d'Input).
  • Liste déroulante : la flèche du navigateur est remplacée par un chevron de 14 px en fg-muted, à 12 px du bord.
  • Cases à cocher et boutons radio : 16 px, en accent quand cochés, fond field et bordure fg-muted sinon. Désactivée, une case passe au gris, cochée ou non.
  • Interrupteur : piste ronde de 38 × 22 px, accent quand activé, line-strong quand éteint, bouton blanc de 16 px sans ombre. Verrouillé, la piste s'estompe et le libellé passe en fg-subtle.

Champs de sélection

  • Select : <select> HTML natif. À utiliser quand la liste tient à l'écran et ne dépasse pas une dizaine d'entrées, statuts, types, langues. Pas de recherche.
  • Combobox : équivalent avec recherche. Trigger identique, champ de filtrage en tête, navigation au clavier. À utiliser dès que la liste devient longue, par exemple pour assigner un utilisateur à une ressource. Le filtre couvre label et value, et chaque option peut afficher un hint secondaire grisé. Son panneau est ancré dans un portail, donc la liste passe par-dessus une modale au lieu d'être rognée par son corps défilant.

Les deux respectent les mêmes conventions de label, description et erreur qu'Input. Pas de composant enveloppant.

Onglets et navigation secondaire

  • Onglets (TabNav) : soulignés, 13 px, 20 px entre deux onglets. Onglet actif : trait de 2 px en primary et texte en graisse 600. Un compteur se pose en pastille ronde de 11 px sur line-soft.
  • Sélecteur segmenté (FilterTabs) : piste line-soft au rayon 8 px, 3 px de marge, segments au rayon 6 px, segments de 28 px en 12,5 px graisse 500. Le segment actif est en surface avec une ombre très légère ; en sombre, il prend line-strong.
  • Fil d'Ariane : sans boîte, 13 px, séparé par des « / ». Éléments en fg-muted, dernier élément en fg graisse 500. Le survol fonce le texte, sans fond.
  • Pagination : cases de 30 px aux coins de 8 px, chiffres en 12,5 px graisse 500. Page courante en fond primary, les autres en texte secondaire, flèches précédente et suivante en boutons bordés.

Cartes et panneaux

Fond surface, bordure 1 px line, rayon 12 px, padding interne de 16 à 20 px, pas d'ombre. Le titre, en 15 px graisse 600, se pose directement au-dessus du contenu, sans filet. Le pied d'actions est séparé par un filet line-soft.

Une carte de statut, rare, prend le fond -soft, la bordure -line et le texte -ink du statut.

Carte de statistique : libellé discret en 12,5 px fg-muted, chiffre en 24 px graisse 700, interlettrage −0,03 em, lien optionnel en accent, « Voir le détail → ». Pas de tuile d'icône colorée. Le chiffre reste à l'encre, sauf pour un état qui demande l'attention : « En retard » en rouge.

Les pages d'authentification font exception : carte de 380 px isolée au centre de l'écran, gouttière de 32 px, logo au-dessus de la carte, titre en 20 px graisse 700, bouton principal pleine largeur de 40 px, lien secondaire centré sous le formulaire, en accent. Leurs champs ne portent pas d'astérisque, parce que tous sont requis. Voir Apparence des pages d'authentification.

  • Menu déroulant : fond surface, bordure line (line-strong en sombre), rayon 12 px, 5 px de marge intérieure, ombre. Items de 32 px aux coins de 6 px, libellé de 13 px graisse 400, icône de 16 px, fond surface-muted au focus. Séparateur line-soft. L'action destructrice vient à la fin, en danger-hover (danger en sombre), fond danger-soft au focus. « Se déconnecter » n'est pas une action destructrice : il reste dans la couleur des autres items. La déconnexion ne détruit rien et se refait en un clic, et un rouge posé partout ne signale plus la suppression.
  • Fenêtre : voile noir à 42 %, sans flou, fenêtre surface au rayon 12 px avec une ombre, marge intérieure de 20 px. Titre en 16 px graisse 700, explication en 13 px fg-secondary, boutons de 34 px alignés à droite : Annuler en secondaire, puis l'action. Pas de filet au repos. Un corps qui défile fait apparaître un filet line-soft sous le titre une fois défilé, et au-dessus des boutons tant qu'il reste du contenu dessous. La fenêtre de confirmation fait 300 px, sans croix de fermeture.
  • Toast : fond encre (primary), texte primary-fg en 13 px, rayon 12 px, ombre. L'icône de statut est nue, sans disque, dans la teinte du statut. Titre facultatif, action soulignée facultative à droite. Pas de dégradé ni de flou.
  • Alerte dans la page : fond -soft du statut, bordure -line, texte -ink, rayon 12 px, icône de 16 px à gauche, texte de 13 px. L'info prend le fond pâle de l'accent.

Boutons d'action des formulaires

Le placement dépend du contexte.

Formulaire pleine page : utiliser UnsavedChangesBar.

  • La barre apparaît dès qu'une modification est détectée et disparaît au retour à l'état initial.
  • Annuler est secondaire et ramène le formulaire à son état initial sans naviguer.
  • Enregistrer est primaire et désactivé pendant l'enregistrement.
  • Pas de bouton de fermeture : tant qu'il y a des modifications, l'utilisateur choisit explicitement.
  • Le hook sous-jacent bloque toute navigation par lien tant qu'il reste des modifications, et fait se secouer la barre à chaque tentative.
  • Après un enregistrement réussi, rester sur la page. Pour une création, rediriger vers la page d'édition de la ressource créée : l'utilisateur reste avec la ressource, pas dans le listing.
  • Aucun bouton inline en sidebar ou en pied de carte.

Le détail de l'API et la gestion de la navigation interne non liée sont dans Composants et Hooks.

FloatingActionBar reste réservée aux actions d'une sélection. Elle apparaît dès le premier élément sélectionné quand la sélection est explicite, cases à cocher à l'appui : l'utilisateur qui vient de cocher attend une suite. Elle ne porte que les actions qui ont un sens sur un lot, le reste vivant dans le menu de la ligne.

Formulaire en modale : garder les boutons dans le pied de la modale. Une modale est déjà contextualisée, l'utilisateur ne peut pas la perdre, et sa croix sert d'annulation explicite.

Tableaux

  • Le tableau vit dans une carte aux coins de 12 px. En clair, l'en-tête prend un fond gris très pâle, surface-hover, et suit les coins de la carte quand le tableau l'ouvre. En sombre, il n'a pas de fond : la carte noire suffit. En-tête en 12 px graisse 400 fg-muted, sans majuscules, séparé des lignes par une bordure line. La colonne triée passe en fg.
  • La carte ne coupe jamais son débordement (overflow: hidden) : un menu de ligne doit pouvoir en sortir. Les coins arrondis viennent des cellules de la dernière ligne.
  • Gouttière de 20 px à gauche et à droite, sur l'en-tête, les cellules et la barre de pagination.
  • Lignes à fond surface, survol légèrement grisé. Une ligne cochée prend accent-soft.
  • Lignes séparées par line-soft, pas de bordure sur la dernière.
  • Cellules en 13 px fg-secondary, en IBM Plex Sans. Montants alignés à droite. Seuls les numéros, codes et dates techniques passent en IBM Plex Mono.
  • Actions à l'extrême droite, en boutons icône discrets.
  • Au-delà de deux actions, ou dès qu'une est destructive, elles passent dans un DropdownMenu plutôt que de s'aligner. Six icônes dans une cellule ne se lisent pas, et une suppression alignée avec les autres se clique par erreur.
  • Une liste où le clic droit est le geste attendu, un explorateur de fichiers par exemple, peut offrir le même menu au pointeur avec ContextMenu. En plus du bouton à trois points, jamais à sa place : une action qui n'existe qu'au clic droit ne se découvre pas et ne s'atteint pas au toucher. Voir Composants.
  • Pas de fond alterné.
  • Un rechargement ne vide pas le tableau. Les squelettes, en pulsation et sans dégradé, sont réservés au premier affichage.
  • État vide : icône de 17 px dans un cercle gris de 36 px, titre court en 13 px graisse 600, phrase d'explication en 12 px fg-muted, action optionnelle.
  • Une liste pleine page pose une hauteur plancher, mais seulement tant qu'elle n'a pas de lignes, minHeight sur Table. Le premier chargement et l'état vide gardent ainsi la même allure d'une page à l'autre : les squelettes remplissent cette hauteur, l'état vide s'y centre. Dès que des lignes s'affichent, la carte se termine sous la dernière : une liste de deux lignes ne traîne pas une grande zone vide. Voir Composants.
  • Une sélection se fait à la case à cocher, en tête de ligne, avec une case dans l'en-tête pour la page affichée. Le clic simple sert à ouvrir : une sélection qui ne s'annonce pas ne se découvre pas, et la case est ce qui l'annonce. Ctrl/Cmd + clic et Maj + clic sélectionnent aussi, mais comme un raccourci pour qui le connaît déjà, jamais comme le seul chemin. Voir Composants.
  • Un glisser-déposer double une action du menu, il ne la remplace pas. Déplacer une ligne à la souris est le geste attendu d'un explorateur de fichiers, mais il ne se voit pas avant d'être tenté, il n'existe pas au toucher, et il ne s'atteint pas au clavier. L'action doit donc rester dans le menu de la ligne et dans la FloatingActionBar. La cible sous le curseur se surligne, et le pointeur emporte une pastille qui dit ce qu'il tient.

Style par défaut des cellules. Une colonne sans render est automatiquement enrobée dans le style canonique, 13 px fg-secondary. Réserver render aux cas qui demandent une composition. Voir Composants.

Pastilles de statut

Petits éléments inline indiquant un état : rayon complet, 20 px de haut dans un tableau, fond -soft du statut, point de 6 px de la couleur du statut, texte en encre -ink en 12 px graisse 500, en IBM Plex Sans. Pas de bordure. « Envoyée » et l'info prennent l'accent : fond accent-soft, texte et point en accent. « Brouillon » est neutre. « Annulée » est neutre et barrée, sans point. Une étiquette qui n'est pas un état, un type par exemple, se pose sans point.

Les compteurs sont ronds : fond encre pour un compte neutre, accent pour une nouveauté, comme le compteur d'un item de la sidebar. Les avatars sont ronds, initiales en graisse 600 sur fond line (line-strong en sombre).

États interactifs

ÉtatApparence
SurvolFond surface-hover ou control-hover : un cran plus sombre en clair, un cran plus clair en sombre, jamais vers le noir du fond de page.
Focus clavierAnneau de 2 px en accent, décalé de 2 px. Sur un champ : bordure en accent et halo à 18 % (22 % en sombre).
DésactivéBouton plein : fond line-soft, texte fg-subtle. Bouton bordé : bordure pâlie, texte fg-subtle. Champ : fond canvas, bordure line, texte fg-subtle. Curseur interdit.
Chargement d'un boutonCouleurs de la variante gardées, indicateur à la place de l'icône, clic bloqué.
Chargement, premier affichageSquelette de la forme du contenu attendu
Chargement, revalidationLe contenu reste en place, atténué à 60 %, contrôles verrouillés

Toutes les transitions durent 120 ms, courbe ease-out. Aucune animation décorative.

Les widgets ARIA marquent l'état désactivé avec aria-disabled, pas avec disabled. Checkbox et Switch sont des <button> porteurs d'un rôle. L'attribut disabled fait partie de l'état de formulaire que le navigateur restaure au rechargement d'une page : une case rendue désactivée par le serveur, parce que le tableau est encore vide, peut ainsi revenir active avant que React ne prenne la main, et l'hydratation signale alors un écart qu'elle ne répare pas. aria-disabled porte la même information sans passer par cet état, et laisse le contrôle atteignable au clavier. Le gestionnaire de clic reste responsable de bloquer l'action.

Un bouton ordinaire garde disabled : c'est l'attribut juste pour un contrôle de formulaire, et son état ne dépend pas d'une donnée arrivée après le rendu serveur.

Un contenu déjà affiché ne disparaît jamais pour être rechargé. Le squelette répond à l'absence de donnée, pas à l'attente d'une donnée fraîche. Vider une vue le temps d'un aller-retour réseau se lit comme un clignotement, et fait sauter la mise en page à chaque tri ou changement de page.

Ce qu'on n'utilise jamais

  • Dégradés sur les fonds ou les boutons.
  • Barres obliques ou éléments inclinés décoratifs.
  • Ombres sur les cartes, ombres internes.
  • Cartes à bordure colorée uniquement sur le côté gauche.
  • L'accent sur un bouton principal, un titre ou une grande surface.
  • Gras utilisé pour une emphase décorative.
  • Icônes sans fonction claire.
  • Statistiques ou chiffres inventés pour remplir l'espace.
  • Couleurs de fond dans les sections de contenu.

Thème sombre

Le thème sombre est une inversion calibrée : le fond de page passe au noir #000000, les cartes et la sidebar à #0b0b0b, la barre du haut au noir du contenu, les champs et contrôles à #141414 sous une bordure #333333, les textes s'éclaircissent. Le bouton principal s'inverse, fond #fafafa et texte #0a0a0a. L'accent et les statuts s'éclaircissent pour rester lisibles sur le noir, et leurs fonds pâles deviennent des voiles de leur couleur. La structure, les espaces et les proportions restent identiques.

Le sombre est porté par les jetons, pas par les classes. Chaque jeton a une valeur claire et une valeur sombre, et la classe dark sur <html> fait basculer les deux ensemble. Un composant écrit bg-surface text-fg, jamais bg-white dark:bg-neutral-900. C'est ce qui rend l'oubli impossible : sans variante dark: à écrire, il n'y en a pas à oublier. Le variant dark: ne sert plus qu'à ce qui n'est pas une couleur, comme afficher un logo propre au sombre.

Un projet peut n'offrir qu'un mode avec l'option modes du thème. Toutes les bascules disparaissent alors, dans l'admin comme sur les pages publiques.

L'implémentation est décrite dans Thèmes.

Sur cette page