DocumentationDesign System

Design System

La charte graphique d'un projet, les variables CSS qu'elle produit et les classes que les éditeurs écrivent : où vit chaque valeur, ce qu'elle devient dans la page, ce qui est permis et ce qui est refusé.

Mis à jour le

Sur cette page10 sections

Vue d'ensemble

L'apparence d'un projet tient dans un seul fichier, la charte graphique web/configurations/brand.identity.json. Au rendu de chaque page, le framework la convertit en variables CSS (--brand, --text-primary, --space-4, --radius-md...) posées sur :root, puis sur [data-theme="light"] pour le thème clair. Les blocs ne lisent que ces variables : une valeur changée dans la charte change tout le site, sans retoucher un bloc.

L'éditeur de charte graphique, section Couleurs, et l'aperçu de l'accueil du projet.
L'éditeur de charte écrit brand.identity.json ; à droite, l'accueil du projet reçoit les variables qui en sortent.w/[wsSlug]/p/[slug]/edit/brand

Sur ces variables, le paquet @asynccode/ac_tools construit la feuille du design system : un squelette de mise en page, une grille de 12 colonnes, des classes d'espacement, de visibilité, de typographie, de tons, de rayons et d'ombres. Les éditeurs de l'Atelier posent ces classes sur les nœuds des arbres (tree.json, structure.json). Ce qu'aucune classe n'exprime part dans le styleJson du nœud, écrit en jetons et compilé dans le style.css du bloc.

Ce que vous écrivez, ce que la page reçoit

Ce que vous écrivezCe que la page reçoit
web/configurations/brand.identity.jsonUn <style> dans le <head> : :root{...} pour le bloc dark, [data-theme="light"]{...} quand un bloc light existe.
props.className (tree.json), classname (structure.json)Les classes de la feuille du design system, générée au build à partir de la charte et réduite aux classes que le projet cite.
styleJson (tree.json)Des règles entre /* forge:nodes:start */ et /* forge:nodes:end */ dans le style.css du bloc, une règle par nœud stylé.
web/index.cssLe CSS global du projet, placé dans la couche @layer app-globals.
web/configurations/layouts/<id>/style.css, web/configurations/components/<id>/style.cssDu CSS hors couche, inséré page par page pour la mise en page et les blocs que la page monte.

Une règle de bloc n'écrit jamais une couleur, une taille ou un rayon en dur : elle cite une variable. C'est ce qui permet au thème clair et aux retouches de la charte de s'appliquer partout.

Le fichier de charte

La charte vit dans web/configurations/brand.identity.json. Sa seule forme est un conteneur à trois clés : defaultTheme, dark et, en option, light. Le framework lit dark comme base des deux thèmes. Un fichier plat, avec les groupes à la racine, n'est pas lu : getBrand reçoit un bloc vide et n'émet que ses valeurs de repli, sans aucune couleur.

json
{ "defaultTheme": "light", "dark": { "palette": { ... }, "surface": { ... }, "ink": { ... }, "fonts": { ... }, "typography": { ... }, "radii": { ... }, "shadows": { ... }, "motion": { ... }, "spacing": { ... }, "stroke": { ... }, "grid": { ... }, "layers": { ... }, "effects": { ... } }, "light": { "palette": { ... }, "surface": { ... }, "ink": { ... }, "shadows": { ... } } }
CléRôle
defaultThemeThème d'un premier visiteur : "light" ou "dark". Toute autre valeur, ou l'absence, vaut "dark". Le choix enregistré du visiteur passe avant.
darkLe bloc complet. Il fait le thème sombre et sert de base au thème clair.
lightFacultatif. Seulement ce qui change en clair : en pratique palette, surface, ink, shadows, et media pour un logo propre au clair.
id, nameIdentifient la charte. getBrand ne les lit pas.

La fusion du thème clair

Le thème clair n'est pas lu seul : le framework fusionne light sur une copie de dark, en profondeur. Un objet imbriqué se fusionne clé par clé (light.palette.brand remplace dark.palette.brand et garde les autres couleurs), une valeur simple remplace. Le résultat passe par getBrand comme le bloc sombre, et le jeu complet des variables est émis une seconde fois sous [data-theme="light"].

Trois réglages ne se lisent que dans dark : les feuilles de polices (fonts.sans.import, fonts.mono.import, fonts.display.import, chargées une fois dans le <head>), le favicon (media.favicon.url), et les couleurs posées sur le voile (--scrim-brand, --scrim-accent-2, calculées depuis la palette sombre dans les deux thèmes). Une famille de police changée dans light sans feuille chargée retombe sur sa pile de repli.

Les valeurs de la charte sont des données : avant d'entrer dans le <style>, chaque variable passe le même contrôle qu'une valeur de styleJson (section suivante sur le styleJson). Une valeur refusée (un ;, une accolade, un <, un url( ailleurs que dans le logo) est retirée, et ce qui la lit retombe sur son propre repli.

Le schéma, groupe par groupe

Chaque groupe ci-dessous vit dans dark (et, pour ce qui change, dans light). La colonne « Si absent » dit ce que getBrand émet quand la clé manque : une valeur de repli, une variable absente (retirée du <style>), ou une valeur invalide qui casse les déclarations qui la lisent.

palette : les couleurs

Des couleurs hexadécimales à six chiffres (#rrggbb). Chaque couleur produit aussi son canal -rgb.

CléRôleSi absent
brandCouleur primaire (« Primaire » dans l'éditeur) : --brand, --tone-primary.Variable absente
brandSecondaryCouleur secondaire : --brand-secondary, --tone-secondary.Variable absente
accentPremier accent : --accent, alias --ui-violet.Variable absente
accent2Deuxième accent : --accent-2, alias --ui-pink.Variable absente
accent3Troisième accent : --accent-3, alias --ui-cyan.Variable absente
accentSoftAccent doux, pour un fond teinté : --accent-soft, alias --ui-pink-soft.Variable absente
success, warning, error, infoLes quatre états : --success, --warning, --error, --info, et les tons --tone-success, --tone-warning, --tone-danger, --tone-info.Variable absente
brandOnEncre posée sur un aplat brand : --ink-text-on-brand, --tone-primary-on.Calculée : #ffffff ou #0b0b12, la plus contrastée
accentOnEncre posée sur un aplat accent : --ink-text-on-accent.Calculée : #ffffff ou #0b0b12, la plus contrastée
primitiveAccent, fragmentAccentCouleurs de l'interface d'édition : --ui-primitive, --ui-fragment.info pour primitiveAccent, accentSoft pour fragmentAccent

surface : les fonds

CléRôleSi absent
baseFond de page : --bg-primary, --ui-bg-0, --sf-0.Variable absente
panelFond de panneau : --bg-secondary, --ui-bg-panel, --sf-1.Variable absente
elevatedFond surélevé : --bg-tertiary, --ui-bg-elev, --sf-2.Variable absente
bg1--ui-bg-1.base
bg2--ui-bg-2.elevated
bgCardFond de carte : --ui-bg-card, --sf-3.panel
scrimPoint de départ du voile sombre --scrim. Se déclare dans dark seulement.Calculé depuis la plus sombre de base et ink.text

ink : l'encre et les filets

text est une couleur ; les six autres clés sont des opacités, de 0 à 1, appliquées à text. Toutes les nuances de texte et de filet dérivent donc d'une seule encre.

CléRôleSi absent
textEncre principale : --text-primary, --ui-text, --ink-1.#ffffff
muted--text-secondary, --ui-muted, --ink-2.Valeur invalide : rgba(r, g, b, undefined)
dim--text-tertiary, --ui-dim, --ink-3.Valeur invalide : rgba(r, g, b, undefined)
faint--ui-faint.Valeur invalide : rgba(r, g, b, undefined)
lineFilet courant : --ui-line, --line-1.Valeur invalide : rgba(r, g, b, undefined)
lineStrongFilet appuyé : --ui-line-strong, --line-2.Valeur invalide : rgba(r, g, b, undefined)
lineFaintFilet léger : --ui-line-faint, --line-0.Valeur invalide : rgba(r, g, b, undefined)

fonts : les polices

Trois rôles, sans, mono et display, chacun { family, import, fallback }. family est le nom de la famille. import est l'adresse de sa feuille Google Fonts (https://fonts.googleapis.com/css2?...), insérée en <link rel="stylesheet"> dans le <head>. fallback est la pile de repli, lue pour sans et display, limitée à des lettres, chiffres, espaces, virgules, guillemets et tirets (120 caractères).

L'éditeur de charte, section Typographie : choix des polices et liste des familles proposées.
La typographie dans l'éditeur de charte : une police pour les titres et le texte, une pour les chiffres et les étiquettes, et le champ qui ajoute une police par son nom ou son lien Google Fonts.w/[wsSlug]/p/[slug]/edit/brand
CléRôleSi absent
sans.familyPolice du texte : --font-sans, --ui-font-sans.system-ui
mono.familyPolice à chasse fixe : --font-mono, --ui-font-mono.ui-monospace
display.familyPolice des titres : --font-display.La police sans
sans.import, mono.import, display.importFeuille de la police. Seules les adresses https:// sont chargées, et seulement depuis dark.Aucune feuille : la famille ne s'affiche que si le poste du visiteur l'a
sans.fallback, display.fallbackPile de repli placée après la famille.ui-sans-serif, -apple-system, system-ui, sans-serif

Les démos autorisent fonts.googleapis.com et fonts.gstatic.com dans web/configurations/csp.json. Une police servie depuis un autre hôte doit y être ajoutée, sinon le navigateur bloque sa feuille.

typography : tailles, interlignes, rôles

La typographie est une échelle à 13 crans : 3xs, 2xs, xs, sm, base, lg, xl, 2xl, 3xl, 4xl, 5xl, 6xl, 7xl. Chaque cran porte une taille, un interligne et un interlettrage, parce que la bonne valeur dépend du cran.

CléRôleSi absent
sizes.3xs ... sizes.7xlTaille en px (un nombre) : --text-3xs à --text-7xl.L'échelle par défaut (voir les variables)
leading.3xs ... leading.7xlInterligne sans unité : --leading-3xs à --leading-7xl.L'échelle par défaut
tracking.3xs ... tracking.7xlInterlettrage en em (un nombre) : --tracking-3xs à --tracking-7xl.L'échelle par défaut
trackingScale.tight, normal, wide--tracking-tight, --tracking-normal, --tracking-wide.-0.025, 0, 0.04
weights.regular, medium, semibold, bold--weight-regular, --weight-medium, --weight-semibold, --weight-bold.Variable absente
lineHeights.tight--leading-tight.Variable absente
lineHeights.base--leading-base. C'est cette clé, et non leading.base, qui donne la variable.Variable absente, même si leading.base existe
roles.display, pageTitle, sectionTitle, leadLes quatre rôles de titrage (ci-dessous).display de 36 à 72 px, pageTitle de 32 à 48 px, sectionTitle cran 3xl, lead cran base

Un rôle s'écrit de deux façons. { "step": "xl" } le fixe sur un cran de l'échelle. { "min": 56, "max": 96 } le rend fluide entre 375 et 1280 px de large, avec min > 0 et max >= min. Les deux formes acceptent leading et tracking pour forcer l'interligne et l'interlettrage. Sans eux, un rôle fluide prend ceux du plus grand cran dont la taille ne dépasse pas max. Un rôle mal formé reprend sa valeur par défaut.

radii : les coins

CléRôleSi absent
radiusLe cadran : un nombre en px dont se déduisent --radius-sm, --radius-md, --radius-lg et --radius-xl (voir les calculs).14
pillRayon des pastilles : --radius-full, --radius-badge, --radius-tag.9999

radius doit être un nombre JSON : "6", écrit en chaîne, est ignoré et le cadran revient à 14. Des clés radii.sm, radii.md, radii.lg ou radii.xl ne sont pas lues.

shadows : les ombres

Des valeurs box-shadow complètes, par exemple 0 4px 12px rgba(0, 0, 0, 0.32), 0 2px 4px rgba(0, 0, 0, 0.3).

CléRôleSi absent
xs, sm, md, ring--shadow-xs, --shadow-sm, --shadow-md, --shadow-ring.soft
soft--shadow-soft, repli des autres.Variable absente
lg--shadow-lg.Variable absente
xl--shadow-xl.lg, puis soft
brandOmbre teintée de la marque : --shadow-brand.Variable absente

motion : les courbes et les durées

CléRôleSi absent
easeOut--ease-out.Variable absente
easeSpring--ease-spring.easeOut
easeInOut--ease-in-out.easeOut
fast--duration-fast, une durée CSS ("120ms").Variable absente
base--duration-base.Variable absente
slow--duration-slow.base

spacing : l'échelle d'espacement

Des clés numériques, écrites en chaîne dans le JSON, vers des px : "0", "1", "2", "3", "4", "5", "6", "8", "10", "12", "16", "20", "24", "32", "40". Chaque clé n donne --space-n ; la clé "1" donne aussi l'unité --u.

CléRôleSi absent
0, 1, 2, 3, 4, 6, 8, 12, 16--space-0 à --space-16, et --u pour la clé 1.Aucun repli : la valeur devient undefinedpx
5, 10, 20, 24, 32, 40--space-5, --space-10, --space-20, --space-24, --space-32, --space-40.20, 40, 80, 96, 128, 160

stroke : les traits

CléRôleSi absent
thinTrait fin, valeur CSS complète ("0.5px") : --stroke-thin.Variable absente
baseTrait courant : --stroke-base.Variable absente

grid : la grille et les largeurs

CléRôleSi absent
gutterGouttière en px : --gutter.24
columnsNombre de pistes de .grid : --cols. Les classes de span s'arrêtent à 12.12
containers.sm, md, lg, xl, xxlLargeur max de .container à chaque palier : --container-sm à --container-xxl.540, 720, 960, 1140, 1320
narrowColonne de lecture : --container-narrow.700
wideLargeur large : --container-wide.1600

layers : l'empilement

CléRôleSi absent
sticky, banner, overlay, dropdown, modal, popover, toast, tooltip--z-sticky à --z-tooltip.100, 200, 300, 400, 500, 600, 700, 800

effects : la matière des surfaces

CléRôleSi absent
surfaceMatière par défaut d'un data-style vide : flat, outlined, elevated, soft ou glass. Inscrite dans la feuille du design system au build.flat
glassBlurFlou de la matière glass, en px : --glass-blur, --surface-filter.14
glassAlphaOpacité du fond glass : --glass-alpha.0.62
enableShadows, enableGradients, enableRoundedLus par aucune variable : sans effet.-

media : logo et favicon

CléRôleSi absent
logo{ url, width, height } : le logo du thème. Donne --brand-logo-url et, si width et height sont des nombres, --brand-logo-ratio.Pas de logo : le nom de marque s'affiche seul
logoDarkLogo pris quand logo n'a pas d'url.-
favicon{ url } : l'icône de l'onglet, lue dans dark seulement.Pas de <link rel="icon">

Les groupes sans repli

Un manque produit l'un de trois effets :

  • Variable absente : une couleur de palette ou de surface, les ombres soft, lg et brand, motion.easeOut, fast et base, weights.*, lineHeights.*, stroke.*. La variable n'est pas émise ; une déclaration qui la lit par var() sans repli devient invalide, et la propriété reprend sa valeur héritée ou initiale.
  • Valeur invalide : spacing pour les clés 0, 1, 2, 3, 4, 6, 8, 12, 16 (--space-4: undefinedpx, --u: undefinedpx), et les opacités de ink (rgba(243, 241, 234, undefined)). La variable existe mais ne vaut rien : chaque marge, retrait, gouttière ou filet qui la lit disparaît.
  • Canal noir : le -rgb d'une couleur absente, ou écrite autrement qu'en #rrggbb (#fff, rgb(...), un nom), vaut 0, 0, 0.

typography.sizes et radii ont un repli : une taille manquante reprend l'échelle par défaut, un radius manquant vaut 14. Les deux groupes à remplir en entier sont spacing et ink.

Référence des variables CSS

getBrand produit 249 variables par thème, toujours sous les mêmes noms ; celles dont la source manque sont retirées du <style>. Les tableaux ci-dessous les donnent toutes, famille par famille, avec leur source dans la charte. Une variable en -rgb porte les trois canaux séparés par des virgules (210, 58, 79) et s'emploie dans rgba(var(--brand-rgb), 0.2).

Marque et accents

VariableSource dans la charteRôle
--brand, --brand-rgbpalette.brandCouleur primaire.
--brand-secondary, --brand-secondary-rgbpalette.brandSecondaryCouleur secondaire.
--accent, --accent-rgbpalette.accentPremier accent.
--accent-2, --accent-2-rgbpalette.accent2Deuxième accent.
--accent-3, --accent-3-rgbpalette.accent3Troisième accent.
--accent-soft, --accent-soft-rgbpalette.accentSoftAccent doux, pour un fond teinté.
--accents-mutedpalette.accent, accent2, accent31 quand les trois accents sont presque gris (chroma OKLCH < 0,04), sinon 0.
--ink-text-on-brandpalette.brandOnEncre lisible sur un aplat --brand.
--ink-text-on-accentpalette.accentOnEncre lisible sur un aplat --accent.

Alias de l'interface

Les mêmes couleurs sous les noms que lisent les fragments du framework. Elles basculent avec le thème comme les autres.

VariableSource dans la charteRôle
--ui-violet, --ui-violet-rgbpalette.accentAlias de --accent.
--ui-pink, --ui-pink-rgbpalette.accent2Alias de --accent-2.
--ui-pink-soft, --ui-pink-soft-rgbpalette.accentSoftAlias de --accent-soft.
--ui-cyanpalette.accent3Alias de --accent-3.
--ui-primitive, --ui-primitive-rgbpalette.primitiveAccentCouleur d'édition, repli palette.info.
--ui-fragmentpalette.fragmentAccentCouleur d'édition, repli palette.accentSoft.

États

VariableSource dans la charteRôle
--success, --success-rgbpalette.successRéussite.
--warning, --warning-rgbpalette.warningAvertissement.
--error, --error-rgbpalette.errorErreur.
--info, --info-rgbpalette.infoInformation.
--ui-green, --ui-amber, --ui-red, --ui-bluepalette.success, warning, error, infoAlias des quatre états, dans cet ordre.

Tons

Six tons sémantiques, chacun décliné en six variables. Dans le second tableau, * vaut primary, secondary, success, warning, danger ou info.

VariableSource dans la charte
--tone-primarypalette.brand
--tone-secondarypalette.brandSecondary
--tone-successpalette.success
--tone-warningpalette.warning
--tone-dangerpalette.error
--tone-infopalette.info
VariableRôle
--tone-*L'aplat du ton.
--tone-*-rgbSes canaux.
--tone-*-onEncre lisible sur l'aplat (palette.brandOn pour primary).
--tone-*-subtleLe ton en lavis : rgba(r, g, b, 0.12).
--tone-*-on-subtleLe ton ajusté pour se lire sur son lavis (4,5:1).
--tone-*-emphasisL'aplat poussé de 15 % vers son encre, pour le survol et l'appui.
--tone-neutral-rgbFixe : 140, 140, 160.

Surfaces et voile

VariableSource dans la charteRôle
--bg-primary, --bg-primary-rgbsurface.baseFond de page.
--bg-secondary, --bg-secondary-rgbsurface.panelFond de panneau.
--bg-tertiary, --bg-tertiary-rgbsurface.elevatedFond surélevé.
--ui-bg-0, --ui-bg-0-rgbsurface.baseAlias de --bg-primary.
--ui-bg-1, --ui-bg-1-rgbsurface.bg1Repli surface.base.
--ui-bg-2, --ui-bg-2-rgbsurface.bg2Repli surface.elevated.
--ui-bg-card, --ui-bg-card-rgbsurface.bgCardFond de carte, repli surface.panel.
--ui-bg-panelsurface.panelAlias de --bg-secondary.
--ui-bg-elevsurface.elevatedAlias de --bg-tertiary.
--scrim, --scrim-rgbsurface.scrimVoile sombre posé sur une image ou une bannière, le même dans les deux thèmes.
--scrim-inksurface.scrimEncre lisible sur le voile.
--scrim-brand, --scrim-brand-inkpalette.brand (dark)La marque éclaircie pour se lire sur le voile, et son encre.
--scrim-accent-2palette.accent2 (dark)Le deuxième accent, ajusté de même.

Encre et filets

VariableSource dans la charteRôle
--text-primary, --text-primary-rgbink.textTexte principal.
--text-secondaryink.mutedTexte secondaire : ink.text à l'opacité muted.
--text-tertiaryink.dimTexte tertiaire.
--ui-text, --ui-text-rgbink.textAlias de --text-primary.
--ui-mutedink.mutedAlias de --text-secondary.
--ui-dimink.dimAlias de --text-tertiary.
--ui-faintink.faintTexte très effacé.
--ui-lineink.lineFilet courant.
--ui-line-strongink.lineStrongFilet appuyé.
--ui-line-faintink.lineFaintFilet léger.

Polices

VariableSource dans la charteRôle
--font-sansfonts.sans'Famille', <fallback>.
--font-displayfonts.displayFamille des titres ; la police sans à défaut.
--font-monofonts.mono'Famille', ui-monospace, Menlo, Consolas, monospace.
--ui-font-sansfonts.sans.familyMême famille, pile fixe ui-sans-serif, system-ui, sans-serif.
--ui-font-monofonts.mono.familyMême famille, pile fixe ui-monospace, Menlo, monospace.

Échelle typographique

Chaque cran donne trois variables, lues dans typography.sizes, typography.leading et typography.tracking. Valeurs par défaut quand la clé manque :

CranVariablesPar défaut (taille, interligne, interlettrage)
3xs--text-3xs, --leading-3xs, --tracking-3xs10px, 1.4, 0.02em
2xs--text-2xs, --leading-2xs, --tracking-2xs11px, 1.45, 0.015em
xs--text-xs, --leading-xs, --tracking-xs12px, 1.33, 0.01em
sm--text-sm, --leading-sm, --tracking-sm14px, 1.43, 0em
base--text-base, --leading-base, --tracking-base16px, 1.5, 0em
lg--text-lg, --leading-lg, --tracking-lg18px, 1.56, 0em
xl--text-xl, --leading-xl, --tracking-xl20px, 1.4, -0.01em
2xl--text-2xl, --leading-2xl, --tracking-2xl24px, 1.33, -0.015em
3xl--text-3xl, --leading-3xl, --tracking-3xl30px, 1.2, -0.02em
4xl--text-4xl, --leading-4xl, --tracking-4xl36px, 1.11, -0.025em
5xl--text-5xl, --leading-5xl, --tracking-5xl48px, 1, -0.03em
6xl--text-6xl, --leading-6xl, --tracking-6xl60px, 1, -0.035em
7xl--text-7xl, --leading-7xl, --tracking-7xl72px, 1, -0.04em

--leading-base vient de typography.lineHeights.base, pas de leading.base : sans lineHeights.base, la variable n'est pas émise du tout, et fs-base perd son interligne.

Rôles de titrage

VariableSource dans la charteRôle
--text-display, --leading-display, --tracking-displaytypography.roles.displayTitre d'accroche. Par défaut fluide de 36 à 72 px.
--text-page-title, --leading-page-title, --tracking-page-titletypography.roles.pageTitleTitre de page. Par défaut fluide de 32 à 48 px.
--text-section-title, --leading-section-title, --tracking-section-titletypography.roles.sectionTitleTitre de section. Par défaut le cran 3xl.
--text-lead, --leading-lead, --tracking-leadtypography.roles.leadChapeau sous un titre. Par défaut le cran base.

Graisses, interlignes, interlettrage

VariableSource dans la charteRôle
--weight-regular, --weight-medium, --weight-semibold, --weight-boldtypography.weightsGraisses nommées.
--leading-tighttypography.lineHeights.tightInterligne serré.
--leading-basetypography.lineHeights.baseInterligne du texte courant.
--tracking-tight, --tracking-normal, --tracking-widetypography.trackingScaleInterlettrages nommés, en em.

Rayons

VariableSource dans la charteRôle
--radius-smradii.radius - 8Petit coin.
--radius-mdradii.radius - 4Coin moyen.
--radius-lgradii.radiusGrand coin, la valeur du cadran.
--radius-xlradii.radius + 4Très grand coin.
--radius-fullradii.pillPastille.
--radius-btn, --radius-inputradii.radius - 8Boutons et champs de saisie.
--radius-btn-lgradii.radius - 4Grand bouton.
--radius-badge, --radius-tagradii.pillBadges et étiquettes.

Ombres

VariableSource dans la charteRôle
--shadow-xsshadows.xsÉlévation minimale.
--shadow-smshadows.smÉlévation faible.
--shadow-softshadows.softOmbre diffuse, repli des autres.
--shadow-mdshadows.mdÉlévation moyenne.
--shadow-lgshadows.lgÉlévation forte.
--shadow-xlshadows.xlÉlévation maximale.
--shadow-ringshadows.ringAnneau d'un pixel.
--shadow-brandshadows.brandOmbre teintée de la marque.

Mouvement

VariableSource dans la charteRôle
--ease-outmotion.easeOutCourbe de sortie.
--ease-springmotion.easeSpringCourbe « spring ».
--ease-in-outmotion.easeInOutCourbe d'entrée et de sortie.
--duration-fastmotion.fastDurée courte (survol).
--duration-basemotion.baseDurée courante.
--duration-slowmotion.slowDurée longue.

Espacement et traits

VariableSource dans la charteRôle
--space-0, --space-1, --space-2, --space-3, --space-4, --space-5, --space-6, --space-8, --space-10, --space-12, --space-16, --space-20, --space-24, --space-32, --space-40spacing.<n>Échelle d'espacement, en px.
--uspacing.1Unité de base.
--stroke-thinstroke.thinTrait fin.
--stroke-basestroke.baseTrait courant.

Échelles numérotées

Des alias numérotés des mêmes valeurs, lus par la feuille du design system.

VariableSource dans la charteRôle
--sf-0, --sf-1, --sf-2, --sf-3surface.base, panel, elevated, bgCardLes quatre étages de surface.
--line-0, --line-1, --line-2ink.lineFaint, line, lineStrongLes trois filets.
--ink-1, --ink-2, --ink-3ink.text, muted, dimLes trois encres.

Grille, couches, effets

VariableSource dans la charteRôle
--guttergrid.gutterGouttière, en px.
--colsgrid.columnsPistes de .grid.
--container-sm, --container-md, --container-lg, --container-xl, --container-xxlgrid.containersLargeur de .container par palier.
--container-narrowgrid.narrowColonne de lecture.
--container-widegrid.wideLargeur large (frame-wide).
--z-sticky, --z-banner, --z-overlay, --z-dropdown, --z-modal, --z-popover, --z-toast, --z-tooltiplayersOrdre d'empilement.
--glass-blur, --surface-filtereffects.glassBlurFlou de la matière glass (14px, blur(14px)).
--glass-alphaeffects.glassAlphaOpacité du fond glass.

Logo

VariableSource dans la charteRôle
--brand-logo-urlmedia.logo.urlurl("..."), pour un background-image.
--brand-logo-displaymedia.logoinline-block avec un logo, none sans.
--brand-logo-ratiomedia.logo.width, heightRapport largeur sur hauteur, 4 décimales.
--brand-name-displaymedia.logo.width, heightnone quand le ratio est connu, sinon inline.
--brand-wordmark-displaymedia.logo.width, heightinline-block quand le ratio est connu, sinon none.

Les cinq variables du logo et --accents-muted ne sont pas acceptées dans un styleJson. Le logo n'entre dans le CSS que par --brand-logo-url : aucune autre valeur url( n'est admise.

Variables de contexte

Celles-ci ne viennent pas de getBrand : la feuille du design system les pose selon le contexte du nœud. Elles sont permises dans un styleJson.

VariableSource dans la charteRôle
--surface, --surface-up[data-surface]Fond du niveau courant et du niveau au-dessus (--sf-1 à --sf-3 selon la profondeur).
--on-surface, --on-surface-2[data-surface]Encre principale et secondaire (--text-primary, --text-secondary).
--line[data-surface]Filet du niveau (--line-1, puis --line-2).
--shadow[data-surface]Ombre du niveau (--shadow-sm, --shadow-md, --shadow-lg).
--radius, --pad[data-surface]Rayon (--radius-md) et retrait (--space-4) d'une boîte.
--radius-inner[data-surface]--radius moins --pad, borné à 0 : le rayon d'un enfant collé au bord.
--container-wgrid.containersLargeur courante de .container : 100%, puis --container-sm à --container-xxl à partir de chaque palier d'écran.
--shell-clearweb/index.cssDégagement sous une barre fixe, lu par scroll-padding-top (84 px si le projet ne le pose pas).

--tone, --tone-rgb, --tone-on, --control-h, --control-pad-x, --control-font-size, --control-radius et --control-gap passent le contrôle d'un styleJson, mais ni getBrand ni la feuille du design system ne les définissent : sans un parent qui les pose, ils ne valent rien.

Ce que getBrand calcule

La plupart des variables recopient une valeur de la charte. Celles-ci sont calculées ; les connaître évite de les écrire à la main.

Le canal rgb

-rgb découpe une couleur #rrggbb en trois entiers séparés par une virgule et une espace. C'est la forme qui donne une transparence tirée de la charte, comme dans ce bloc d'une démo :

css
[data-forge-id="aboutHeader--crumbHome"][data-forge-id="aboutHeader--crumbHome"] { color: rgba(var(--bg-primary-rgb),0.72); text-decoration: none; }

Toute autre écriture (#fff, rgb(...), red) donne 0, 0, 0, et l'encre calculée sur cette couleur devient #ffffff.

Les encres lisibles

--ink-text-on-brand, --ink-text-on-accent et --tone-*-on comparent le contraste du blanc et du noir sur l'aplat et gardent le meilleur : #ffffff ou #0b0b12. palette.brandOn et palette.accentOn imposent une autre encre.

Les tons

  • -subtle : l'aplat à 12 % d'opacité.
  • -emphasis : l'aplat mélangé à 15 % vers son encre -on. Une marque claire fonce au survol, une marque sombre s'éclaircit, dans les deux thèmes.
  • -on-subtle : le ton, poussé par pas de 5 % vers l'encre lisible jusqu'à 4,5:1 de contraste contre surface.base couvert du lavis (20 pas au plus).

Le voile

--scrim part de surface.scrim, ou à défaut de la plus sombre de surface.base et ink.text, assombrie de moitié vers le noir. Il est ensuite assombri par pas de 5 % jusqu'à ce qu'un texte blanc garde 7:1 de contraste sur ce voile posé à 72 % sur du blanc. --scrim-brand et --scrim-accent-2 sont la marque et le deuxième accent de la palette sombre, éclaircis jusqu'à 4,5:1 sur le voile.

Le cadran de rayon

radii.radius est le seul réglage des coins, avec pill. Les crans s'en déduisent à pas de 4 px, bornés à 0 :

Cadran radii.radius--radius-sm, -btn, -input--radius-md, -btn-lg--radius-lg--radius-xl
00px0px0px4px
60px2px6px10px
146px10px14px18px
2012px16px20px24px
radius: 0
--radius-sm0px
--radius-md0px
--radius-lg0px
--radius-xl4px
radius: 6
--radius-sm0px
--radius-md2px
--radius-lg6px
--radius-xl10px
radius: 14
--radius-sm6px
--radius-md10px
--radius-lg14px
--radius-xl18px
radius: 20
--radius-sm12px
--radius-md16px
--radius-lg20px
--radius-xl24px

14 est la valeur par défaut. --radius-full, --radius-badge et --radius-tag valent radii.pill, 9999 par défaut.

Les tailles fluides

Un rôle { min, max } devient un clamp() qui croît en ligne droite de min à 375 px de large jusqu'à max à 1280 px : pente (max - min) / 905, origine min - pente x 375. Avec min égal à max, la valeur est fixe. Les rôles de la démo de l'exemple complet donnent :

css
--text-display: clamp(56px, 4.42vw + 39.43px, 96px); --text-page-title: clamp(48px, 3.536vw + 34.74px, 80px); --text-section-title: clamp(38px, 2.431vw + 28.88px, 60px); --text-lead: 20px;

Les accents gris

Quand accent, accent2 et accent3 ont tous une chroma OKLCH inférieure à 0,04, --accents-muted vaut 1. Si c'est vrai dans les deux thèmes, la balise <html> reçoit data-accents="muted", qu'une règle peut cibler pour une charte monochrome.

Clair et sombre

Le Layout émet un <style> en tête de page : :root porte les variables du bloc dark, puis, si le fichier a un bloc light, [data-theme="light"] porte le jeu complet recalculé pour le clair. Les deux sélecteurs pèsent autant ; le second, écrit après, l'emporte dès que <html> porte data-theme="light".

css
:root{--brand:#d23a4f;--brand-rgb:210, 58, 79;...;--bg-primary:#1a211c;...} [data-theme="light"]{--brand:#c42a3f;--brand-rgb:196, 42, 63;...;--bg-primary:#f2f4f5;...}

Les couleurs que le client retouche depuis l'administration de contenu sont émises dans un second <style>, après celui-ci, sur :root et :root[data-theme="light"] : elles l'emportent dans les deux thèmes.

L'accueil de la démo de pâtisserie en thème clair.
Thème clair : <html data-theme="light">, les variables du bloc light.
L'accueil de la démo de pâtisserie en thème sombre.
Thème sombre : sans attribut, les variables du bloc dark.

Le choix du thème avant l'affichage

Un script en ligne, dans le <head>, fixe le thème avant la première peinture, sans flash :

  • localStorage theme-mode vaut auto : clair de 7 h à 18 h 59 (heure du poste), sombre sinon.
  • Sinon localStorage theme-preference : light ou dark, le dernier choix du visiteur.
  • Sinon defaultTheme de la charte.

Clair : <html data-theme="light">. Sombre : l'attribut est retiré, le sombre est l'état sans attribut. Le script écrit aussi data-default-theme. Le contrôleur ThemeManager, monté une fois, réapplique le thème après chaque navigation ; le fragment ThemeSwitcher écrit theme-mode et theme-preference.

Projet sombre seul

  • Ne mettez pas de bloc light. Aucune règle [data-theme="light"] n'est émise : même si l'attribut est posé, les variables restent celles de dark.
  • Laissez defaultTheme à "dark", ou absent.
  • Ne montez pas de ThemeSwitcher.
  • Ne figez pas les surfaces dans web/index.css (--bg-primary posé sur body pour forcer le sombre) : c'est la trace d'un bloc light resté dans la charte. Retirez le bloc.

Projet clair par défaut

"defaultTheme": "light" avec un bloc light : un premier visiteur voit le clair, le sombre reste accessible par la préférence. "defaultTheme": "light" sans bloc light pose l'attribut mais ne change aucune variable : le site reste sombre.

Une règle écrite à la main sous [data-theme='light'] ne vaut que pour le thème clair et se maintient à part. Dans un bloc, préférez les variables : elles basculent seules.

Le système de classes

La feuille du design system est générée par @asynccode/ac_tools (designSystemCss) à partir de la charte du projet, au build du site, et insérée dans le CSS global de chaque page. Elle déclare l'ordre des couches de toute la cascade :

css
@layer ds-reset, ds-tokens, ds-base, ds-grid, ds-components, app-globals, ds-utilities;

Une couche déclarée plus tard l'emporte sur les précédentes, quelle que soit la spécificité, et tout CSS hors couche (le style.css d'un bloc, d'une vue, d'une mise en page) l'emporte sur toutes les couches. Les utilitaires (ds-utilities) battent donc web/index.css (app-globals) et perdent contre le style.css d'un bloc. Aucune classe du système ne porte !important.

Sur le site publié, la feuille ne garde que les classes dont le nom apparaît comme mot dans un fichier .js, .jsx ou .json de web/configurations ou du framework. Une classe assemblée à partir d'une donnée qui n'est écrite dans aucun fichier (une réponse d'API, par exemple) est retirée : elle s'affiche dans l'Aperçu, qui garde toute la feuille, et disparaît en ligne.

Le squelette

ClasseEffet
containerLargeur 100%, centrée, retrait latéral d'une demi-gouttière, max-width: var(--container-w).
container-fluidPleine largeur, même retrait.
container-narrowColonne de lecture : max-width: var(--container-narrow).
full-bleedSort du conteneur : width: 100vw, d'un bord de la fenêtre à l'autre.
gridGrille CSS de var(--cols, 12) pistes égales, gouttière --gutter. Le parent est le conteneur ds-grid, chaque enfant un conteneur ds-cell.
frame-narrow, frame-page, frame-wideRetrait intérieur qui centre le contenu d'une bande sur --container-narrow, --container-w ou --container-wide ; le fond reste pleine largeur.
frame-band-soft, frame-band-strong, frame-band-brandFond de bande : --bg-secondary, --bg-tertiary, --tone-primary-subtle.
frame-space-s, frame-space-m, frame-space-lRespiration verticale fluide d'une bande, un clamp() entre deux crans d'espacement.

--container-w vaut 100%, puis, à partir de 576, 768, 992, 1200 et 1400 px de large, --container-sm, --container-md, --container-lg, --container-xl et --container-xxl.

La grille de 12 colonnes

Le parent porte grid, chaque enfant dit sa largeur en colonnes. Il n'y a pas de cols-N : la grille a toujours 12 pistes (--cols), et « trois colonnes » s'écrit avec trois enfants g-col-4.

ClasseEffet
g-col-1 ... g-col-12L'enfant couvre N pistes (grid-column: auto/span N).
g-col-autoUne piste, placement automatique.
g-start-1 ... g-start-12, g-start-autoPiste de départ de l'enfant (un décalage).
g-col-<bp>-N, g-start-<bp>-NLes mêmes à partir d'un palier de largeur de la grille ; <bp> vaut sm, md, lg, xl ou xxl.
g-order-<bp>-firstL'enfant passe en tête à partir du palier.
g-N, gx-N, gy-NGouttière de la grille (deux axes, colonnes, lignes) prise dans --space-N. Ne marchent que sur .grid.
gap-N, gapx-N, gapy-Ngap, column-gap, row-gap sur n'importe quelle boîte flex ou grille.
grid-balance, grid-balance-centerSur le parent : quand la dernière rangée est incomplète, ses enfants g-col-6, g-col-4 ou g-col-3 s'élargissent pour la remplir, ou se centrent.

Les crans N de gouttière et d'espacement : 0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40. Exemple réel : une rangée de quatre principes, une colonne pleine sur une grille étroite, deux par rangée à partir de 20rem de grille, quatre à partir de 40rem.

json
{ "id": "items", "primitive": "wrapper", "styleJson": { "base": { "alignItems": "start", "width": "100%" } }, "props": { "className": "grid gap-6 w-100" }, "children": [ { "id": "itemA", "primitive": "wrapper", "props": { "className": "g-col-12 g-col-sm-6 g-col-lg-3 d-flex flex-column align-items-start gap-3" }, "children": [ ... ] }, ... ] }

Deux axes de paliers

Les paliers portent les mêmes noms sur deux axes qui ne se mélangent pas : un md d'écran n'est pas un md de grille.

PalierÉcran (@media)Grille et cellule (@container)
sm576px20rem
md768px30rem
lg992px40rem
xl1200px50rem
xxl1400px60rem
  • Écran : @media (min-width: ...), la largeur de la fenêtre. Les utilitaires à palier (d-md-none, mt-lg-4, order-md-first...) et les blocs sm à xxl d'un styleJson.
  • Grille : @container ds-grid (min-width: ...), la largeur du .grid parent. Seules g-col-<bp>-*, g-start-<bp>-* et g-order-<bp>-first s'y rangent. Une grille posée dans une colonne étroite se replie donc comme sur un petit écran.
  • Cellule : un styleJson qui porte "containerQuery": true écrit ses blocs sm à xxl en @container ds-cell, la largeur de la cellule de grille ou de la bande frame-* qui contient le nœud. cq-order-<bp>-first suit le même axe.

Les utilitaires

Une classe utilitaire fait une seule chose. La forme à palier insère le palier après le préfixe : d-none devient d-md-none, mt-4 devient mt-lg-4. Seules les familles marquées « palier » l'acceptent, et ce palier est celui de l'écran.

ClassesEffet
d-none, d-inline, d-inline-block, d-block, d-flex, d-inline-flex, d-griddisplay, palier. C'est la famille de visibilité : d-none d-md-block masque sous 768 px.
flex-row, flex-column, flex-row-reverse, flex-column-reverse, flex-wrap, flex-nowrap, flex-fill, flex-grow-0, flex-grow-1, flex-shrink-0, flex-shrink-1Direction, retour à la ligne, croissance ; palier.
align-items-<v>, align-self-<v> (start, end, center, baseline, stretch)Alignement sur l'axe secondaire ; palier.
justify-content-<v> (start, end, center, between, around, evenly)Répartition sur l'axe principal ; palier.
m-N, mt-N, mb-N, ms-N, me-N, mx-N, my-N, m-auto ...Marges en --space-N ou auto. t, b : haut, bas ; s, e : début et fin de ligne ; x, y : les deux côtés. Palier.
p-N, pt-N, pb-N, ps-N, pe-N, px-N, py-NRetraits en --space-N, mêmes côtés ; palier.
gap-Ngap en --space-N ; palier.
text-start, text-end, text-center, text-justifyAlignement du texte ; palier.
text-lowercase, text-uppercase, text-capitalize, text-nowrap, text-wrap, text-balance, text-breakCasse et coupure des lignes.
fs-3xs ... fs-7xlTaille, interligne et interlettrage du cran (--text-*, --leading-*, --tracking-*).
fw-regular, fw-medium, fw-semibold--weight-*. Il n'y a pas de fw-bold.
tracking-tight, tracking-normal, tracking-wide--tracking-*.
text-primary, text-secondary, text-success, text-warning, text-danger, text-info, bg-primary ... bg-info, bg-primary-subtle ... bg-info-subtle, border-primary ... border-infoCouleur, fond, lavis ou couleur de bordure du ton (--tone-*, --tone-*-subtle).
text-body, text-muted, bg-surface, bg-surface-up--on-surface, --on-surface-2, --surface, --surface-up.
border, border-top, border-bottom, border-start, border-end, border-0, border-top-0 ...Filet --stroke-base en --line-1, ou retiré.
rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-full, rounded-0--radius-*.
shadow-xs, shadow-sm, shadow-md, shadow-lg, shadow-xl, shadow-none--shadow-*.
position-static, position-relative, position-absolute, position-fixed, position-stickyPositionnement.
top-0, top-50, top-100, bottom-*, start-*, end-*Décalages en %, côtés logiques pour start et end.
w-25, w-50, w-75, w-100, w-auto, h-25 ... h-auto, mw-100, mh-100Largeur et hauteur en % ; w-* et h-* à palier.
overflow-auto, overflow-hidden, overflow-visible, overflow-scrollDébordement.
order-0 ... order-5, order-first, order-lastOrdre d'affichage ; palier d'écran.
opacity-0, opacity-25, opacity-50, opacity-75, opacity-100Opacité.
z-sticky, z-banner, z-overlay, z-dropdown, z-modal, z-popover, z-toast, z-tooltipz-index: var(--z-*).

text-primary colore en --tone-primary, la couleur de marque, et non en --text-primary. Pour l'encre courante, utilisez text-body ; pour l'encre secondaire, text-muted.

Les aides et les recettes

Les aides sont toujours présentes, jamais élaguées : clearfix, text-bg-primary à text-bg-info (fond du ton avec son encre), link-primary à link-info, focus-ring, icon-link et icon-link-hover, translate-middle, translate-middle-x, translate-middle-y, ratio avec ratio-1x1, ratio-4x3, ratio-16x9, ratio-21x9, vstack, hstack, stretched-link, text-truncate, vr, visually-hidden, visually-hidden-focusable.

Des recettes de contenu (table, table-sm, table-striped, table-hover, figure, blockquote, list-unstyled, list-inline, img-thumbnail...), de formulaire (form-label, form-text, invalid-feedback, valid-feedback, form-range) et de composants (alert-primary à alert-info, accordion, modal, offcanvas, list-group, progress, toast, popover, tooltip, breadcrumb, spinner-border, navbar...) complètent la feuille. Les composants sont ajoutés page par page, seulement quand la page cite leur classe. Les familles btn*, badge* et form-control vivent dans le CSS global du framework.

Le contexte : data-surface et data-style

Deux attributs règlent le contexte d'une boîte au lieu de lui donner une couleur. Dans une vue, ils se posent à la racine du nœud ; dans un tree.json, dans props.

  • data-surface vide : le niveau se compte par profondeur, trois au plus. Au premier, --surface vaut --sf-1, --line --line-1 et --shadow --shadow-sm ; au deuxième, --sf-2, --line-2, --shadow-md ; au troisième, --sf-3 et --shadow-lg. data-surface="1", "2" ou "3" impose un niveau absolu, pour une modale ou un menu flottant.
  • data-style peint la boîte avec une matière : fond --surface, bordure --stroke-thin en --line, ombre --shadow.
ValeurEffet
flatNi filet ni ombre.
outlinedFilet --line-2, pas d'ombre.
elevatedPas de filet, ombre --shadow-lg.
softFond --surface-up, pas de filet, ombre --shadow-sm.
glassFond --bg-secondary à l'opacité --glass-alpha sous un reflet --glass-sheen, bord --glass-edge, arête éclairée et ombre --glass-lift, flou --surface-filter. Se voit sur un fond uni ; le reflet suit le fond (blanc sur sombre, givre sur clair).
""Valeur vide : la matière de effects.surface, flat par défaut.

Le vocabulaire de l'inspecteur

@utils/dsVocabulary dérive de cette feuille les familles que l'inspecteur de l'Atelier lit et écrit. Une famille regroupe les classes qui posent les mêmes propriétés (deux classes d'une famille s'excluent) et porte un axe : écran, grille ou aucun. L'inspecteur tient une seule source par nœud et par propriété : poser une classe retire du styleJson les déclarations qui touchent ses propriétés (les états restent), et écrire une valeur hors du système retire la famille de classes correspondante.

À l'écriture d'un tree.json ou d'un structure.json, l'API compare chaque classe au vocabulaire. Sont reconnues les classes du design system, les classes propres du bloc (<id>-..., l'id étant le nom de son dossier), les références $..., btn*, badge*, form-control et under-nav. Toute autre classe est écrite quand même, et la réponse porte un avertissement node "<id>": <classes> outside the design system vocabulary.

styleJson dans un tree.json

Chaque nœud d'un tree.json peut porter, à côté de props, un styleJson : des blocs de déclarations en camelCase, la forme d'un objet style React. Les éditeurs y écrivent ce qu'aucune classe ne couvre : couleurs hors tons, dégradés, filets fins, transitions, états.

BlocCompilé en
baseLa règle du nœud.
hover, focus-visible, active:hover, :focus-visible, :active, dans cet ordre.
before, after::before, ::after.
hover-before, hover-after:hover::before, :hover::after.
sm, md, lg, xl, xxl@media (min-width: 576px) à 1400px, ou @container ds-cell (min-width: 20rem) à 60rem avec containerQuery.
containerQuerytrue, et rien d'autre, fait passer les blocs de palier sur l'axe cellule.

Un bloc de palier ne contient que des déclarations, pas d'état imbriqué. Quand base pose display: grid sans piste intrinsèque (auto, min-content, max-content, fit-content()), la compilation ajoute une règle qui fait de chaque enfant un conteneur ds-cell.

Exemple réel

Le bandeau d'appel à l'action d'une démo : un conteneur au contexte outlined, un lien avec ses états, une liste qui passe en ligne à partir de 992 px.

json
{ "id": "banner", "primitive": "wrapper", "props": { "className": "d-flex flex-row flex-wrap align-items-center justify-content-between gap-6 w-100", "data-style": "outlined", "data-surface": "" }, "styleJson": { "base": { "padding": "var(--pad)", "borderRadius": "var(--radius-sm)", "backgroundColor": "var(--scrim)", "color": "var(--scrim-ink)", "boxShadow": "var(--shadow-lg)" } }, "children": [ ... ] } { "id": "primary", "primitive": "Link", "props": { "href": "/gateaux", "className": "btn btn-primary btn-md d-inline-flex align-items-center justify-content-center gap-2 py-3 px-5" }, "styleJson": { "base": { "whiteSpace": "normal", "textAlign": "center" }, "hover": { "transform": "translateY(-1px)" }, "focus-visible": { "outlineOffset": "2px" } }, "children": [ ... ] } { "id": "milestones", "primitive": "List", "styleJson": { "base": { "display": "flex", "flexDirection": "column", "margin": "0", "padding": "0", "listStyle": "none" }, "lg": { "flexDirection": "row" } }, "props": { "ordered": true }, "children": [ ... ] }

Ce que la compilation écrit dans le style.css de chaque bloc :

css
/* forge:nodes:start */ [data-forge-id="homeCta--root"][data-forge-id="homeCta--root"] { position: relative; overflow: hidden; } [data-forge-id="homeCta--banner"][data-forge-id="homeCta--banner"] { padding: var(--pad); border-radius: var(--radius-sm); background-color: var(--scrim); color: var(--scrim-ink); box-shadow: var(--shadow-lg); } ... [data-forge-id="homeCta--primary"][data-forge-id="homeCta--primary"] { white-space: normal; text-align: center; } [data-forge-id="homeCta--primary"][data-forge-id="homeCta--primary"]:hover { transform: translateY(-1px); } [data-forge-id="homeCta--primary"][data-forge-id="homeCta--primary"]:focus-visible { outline-offset: 2px; } ... /* forge:nodes:end */ /* forge:nodes:start */ ... [data-forge-id="aboutMilestones--milestones"][data-forge-id="aboutMilestones--milestones"] { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; } @media (min-width: 992px) { [data-forge-id="aboutMilestones--milestones"][data-forge-id="aboutMilestones--milestones"] { flex-direction: row; } } ... /* forge:nodes:end */

Propriétés permises

Une clé hors de cette liste est retirée sans erreur. Les noms sont en camelCase et en côtés physiques : paddingLeft, jamais paddingInlineStart.

GroupePropriétés
Boîtemargin, marginTop, marginRight, marginBottom, marginLeft, padding, paddingTop, paddingRight, paddingBottom, paddingLeft, gap, rowGap, columnGap
Taillewidth, minWidth, maxWidth, height, minHeight, maxHeight, aspectRatio, boxSizing, overflow, overflowX, overflowY, contain
Affichage et positiondisplay, position, top, right, bottom, left, inset, zIndex, float, clear, visibility
FlexflexDirection, flexWrap, flexFlow, flex, flexGrow, flexShrink, flexBasis, order, alignItems, alignContent, alignSelf, justifyContent, justifyItems, justifySelf, placeItems, placeContent, placeSelf
GrillegridTemplateColumns, gridTemplateRows, gridTemplateAreas, gridTemplate, gridAutoColumns, gridAutoRows, gridAutoFlow, gridColumn, gridRow, gridArea, gridColumnStart, gridColumnEnd, gridRowStart, gridRowEnd
Typographiecolor, fontFamily, fontSize, fontWeight, fontStyle, fontVariant, lineHeight, letterSpacing, wordSpacing, textAlign, textTransform, textDecoration, textDecorationLine, textDecorationColor, textDecorationStyle, textIndent, textOverflow, textShadow, whiteSpace, wordBreak, overflowWrap, verticalAlign, listStyle, listStyleType, listStylePosition, direction
Fondbackground, backgroundColor, backgroundImage, backgroundPosition, backgroundSize, backgroundRepeat, backgroundClip, backgroundOrigin, backgroundAttachment, backgroundBlendMode
Bordureborder, borderWidth, borderStyle, borderColor, borderRadius, borderTop, borderRight, borderBottom, borderLeft, borderTopWidth, borderRightWidth, borderBottomWidth, borderLeftWidth, borderTopStyle, borderRightStyle, borderBottomStyle, borderLeftStyle, borderTopColor, borderRightColor, borderBottomColor, borderLeftColor, borderTopLeftRadius, borderTopRightRadius, borderBottomRightRadius, borderBottomLeftRadius
Contouroutline, outlineWidth, outlineStyle, outlineColor, outlineOffset
EffetsboxShadow, opacity, filter, backdropFilter, mixBlendMode, transition, transitionProperty, transitionDuration, transitionTimingFunction, transitionDelay, cursor, pointerEvents, userSelect, objectFit, objectPosition, resize, clipPath
Transformationstransform, transformOrigin, transformStyle, perspective, perspectiveOrigin, backfaceVisibility, rotate, scale, translate, willChange
DiverstextWrap, textUnderlineOffset, textDecorationThickness, fontFeatureSettings, fontVariationSettings, accentColor, caretColor, isolation, tabSize, scrollBehavior, scrollSnapType, scrollSnapAlign, scrollMargin, scrollPadding
Animationanimation, animationName, animationDuration, animationTimingFunction, animationDelay, animationIterationCount, animationDirection, animationFillMode, animationPlayState, animationTimeline, animationRange, animationRangeStart, animationRangeEnd
Contenucontent

Les images clés d'une animation ne s'écrivent pas dans un styleJson : elles vivent dans le CSS du bloc, et le styleJson les nomme par animationName.

Valeurs permises

  • Une chaîne ou un nombre, de 512 caractères au plus.
  • Caractères permis : lettres, chiffres, espaces et . , % # ( ) + * ' " / -. Tout autre caractère refuse la valeur, notamment ;, {, }, <, >, \, :, ! (donc !important), @, & et _.
  • url(, @import, expression(, /* et */ sont refusés, ainsi que les mots javascript, vbscript, behavior, -moz-binding, image (donc image-set()), cross-fade, src et brand-logo-url. Une image de fond passe par le choix d'un média, jamais par un styleJson.
  • var() : chaque référence doit nommer un jeton permis, c'est-à-dire une variable des tableaux précédents (sauf --accents-muted et les cinq du logo) ou une variable de contexte. Une référence avec repli (var(--brand, red)) ou imbriquée est refusée. Plusieurs références dans une valeur sont permises (var(--space-2) var(--space-4)), et une référence peut vivre dans une fonction (color-mix(in srgb, var(--scrim-ink) 40%, transparent)).
  • content n'accepte que none, normal ou une chaîne entre guillemets de 64 caractères au plus.
  • Une déclaration refusée est retirée sans message ; le reste du bloc est compilé.

Reteinter un sous-arbre

Dix variables peuvent être redéfinies dans un styleJson pour reteinter un nœud et ses descendants : --brand, --brand-secondary, --accent, --accent-2, --accent-3 et leurs jumelles -rgb. La couleur prend un hexadécimal (3, 4, 6 ou 8 chiffres), un rgb() ou rgba() numérique, ou un jeton permis ; la jumelle -rgb prend un triplet r, g, b. Écrivez toujours la paire, sinon les transparences rgba(var(--brand-rgb), ...) gardent l'ancienne couleur. Aucune autre propriété personnalisée n'est acceptée.

Les variables calculées depuis la marque ne suivent pas : --tone-primary, --ink-text-on-brand ou --tone-primary-subtle gardent la valeur posée sur :root.

Raccourcis de grille

Un nœud peut aussi porter col, offset, order et g, sur le nœud ou dans props. Ils se développent en styleJson à la compilation : col: 6 donne gridColumn: "auto/span 6" ; col: "md-6" ou col: { "base": 12, "md": 6 } visent un palier d'écran ; offset donne gridColumnStart, order donne order, g donne gap: var(--space-N). Le styleJson écrit à la main l'emporte sur le raccourci. Les éditeurs n'écrivent plus ces raccourcis : préférez les classes g-col-*.

La compilation dans style.css

Les éditeurs de l'Atelier et la création d'un bloc compilent l'arbre (compileFragmentNodesCss) et réécrivent la zone du style.css du bloc comprise entre /* forge:nodes:start */ et /* forge:nodes:end */. Tout ce qui est hors de ces deux marques est conservé tel quel. À la première compilation, la zone est ajoutée en fin de fichier, après le CSS déjà écrit.

Chaque nœud stylé reçoit une règle sur son identifiant préfixé par l'id du bloc, <bloc>--<nœud>, écrit deux fois. Ce doublement porte la règle à une spécificité de (0,2,0), au niveau des styles de base des fragments du framework ([data-fragment='Nom'] .titre), que la création d'un bloc recopie au-dessus de la zone : écrite après eux, elle l'emporte. Toutes les instances d'un même bloc partagent l'identifiant, donc la règle.

Au rendu, le bloc est enveloppé dans <div style="display: contents" data-fid="<bloc>">. Seuls la racine de chaque instance et les nœuds qui portent un style effectif reçoivent l'attribut data-forge-id ; le canevas d'édition, lui, le pose sur tous les nœuds.

Le style.css est le seul porteur de ces règles : le rendu n'applique pas le styleJson en ligne. Un styleJson modifié à la main dans tree.json ne change rien tant que la zone n'est pas recompilée, ce que fait un enregistrement depuis un éditeur.

Le piège de la classe d'état

Une classe d'état écrite à la main, .x.is-on, pèse (0,2,0) : autant que la règle générée du nœud. À égalité, l'ordre dans le fichier décide. Écrite avant la zone générée, la règle d'état perd sur toute propriété que le styleJson du nœud pose aussi : si base pose background et que .x.is-on pose aussi background, l'état ne s'affiche jamais.

  • Laissez hors du styleJson les propriétés que l'état change, et écrivez l'état de repos et l'état actif dans le CSS du bloc (.x et .x.is-on).
  • Ou écrivez la règle d'état après /* forge:nodes:end */ : la recompilation conserve ce qui suit la zone.
  • Ou préfixez par [data-fid='<bloc>'], comme ci-dessous, pour passer à (0,3,0). Un tel sélecteur bat aussi les retouches de style posées sur le nœud depuis l'administration de contenu (même sélecteur doublé), et la vérification des lois du design system signale tout sélecteur de plus de deux composés : gardez-le pour les états.
css
[data-fid='tarifFlow'] .tarifFlow-choice.is-on { border-color: var(--brand); background: var(--brand); color: var(--ink-text-on-brand); }

Exemple complet

La charte d'une démo de pâtisserie, raccourcie : un thème sombre complet, un thème clair qui ne change que les couleurs et les ombres, le clair par défaut.

json
{ "defaultTheme": "light", "dark": { "palette": { "brand": "#d23a4f", "brandSecondary": "#e3c49c", "accent": "#e3c49c", "accent2": "#d9a46b", "accent3": "#a9b5ad", "accentSoft": "#3a2328", "success": "#7fd3a0", "warning": "#f0c06a", "error": "#ff8f85", "info": "#9cc6e0" }, "surface": { "base": "#1a211c", "panel": "#212a24", "elevated": "#29332c", "bg1": "#1d2520", "bg2": "#344038", "bgCard": "#202923", "scrim": "#141a16" }, "ink": { "text": "#f3f1ea", "muted": 0.76, "dim": 0.64, "faint": 0.38, "line": 0.13, "lineStrong": 0.22, "lineFaint": 0.07 }, "fonts": { "sans": { "family": "Sofia Sans", "import": "https://fonts.googleapis.com/css2?family=Sofia+Sans:wght@400;500;600;700;800&display=swap" }, "mono": { "family": "Geist Mono", "import": "..." }, "display": { "family": "Sofia Sans Extra Condensed", "import": "..." } }, "typography": { "roles": { "display": { "min": 56, "max": 96 }, "pageTitle": { "min": 48, "max": 80 }, "sectionTitle": { "min": 38, "max": 60 }, "lead": { "step": "xl" } }, "sizes": { "3xs": 10, "2xs": 11, "xs": 12, "sm": 14, "base": 16, ... , "7xl": 72 }, "leading": { "3xs": 1.4, ... , "4xl": 1, "5xl": 0.95, "6xl": 0.95, "7xl": 0.95 }, "tracking": { "3xs": 0.02, ... , "7xl": -0.005 }, "trackingScale": { "tight": -0.02, "normal": 0, "wide": 0.03 }, "weights": { "regular": 400, "medium": 500, "semibold": 700, "bold": 800 }, "lineHeights": { "tight": 1.15, "base": 1.5 } }, "radii": { "radius": 6, "pill": 9999 }, "shadows": { "xs": "0 1px 2px rgba(0, 0, 0, 0.3)", "soft": "0 2px 8px rgba(0, 0, 0, 0.3)", "lg": "0 12px 28px rgba(0, 0, 0, 0.36), 0 4px 8px rgba(0, 0, 0, 0.3)", "brand": "0 8px 20px rgba(196, 42, 63, 0.22)", ... }, "motion": { "easeOut": "cubic-bezier(0.16, 1, 0.3, 1)", "easeSpring": "cubic-bezier(0.22, 1, 0.36, 1)", "easeInOut": "cubic-bezier(0.4, 0, 0.2, 1)", "fast": "120ms", "base": "220ms", "slow": "380ms" }, "spacing": { "0": 0, "1": 4, "2": 8, "3": 12, "4": 16, "5": 20, "6": 24, "8": 32, "10": 40, "12": 48, "16": 64, "20": 80, "24": 96, "32": 128, "40": 160 }, "stroke": { "thin": "0.5px", "base": "1px" }, "grid": { "containers": { "sm": 1180, "md": 1180, "lg": 1180, "xl": 1180, "xxl": 1180 }, "gutter": 24, "columns": 12, "narrow": 700 }, "layers": { "sticky": 100, "banner": 200, ... , "tooltip": 800 }, "effects": { "surface": "soft", "glassBlur": 14, "glassAlpha": 0.62, ... } }, "light": { "palette": { "brand": "#c42a3f", "brandSecondary": "#1f2a24", "accent": "#1f2a24", "accent2": "#8a5a2b", "accent3": "#5f6b63", "accentSoft": "#f6dfe2", "success": "#2e6b45", "warning": "#8a5a00", "error": "#a3231c", "info": "#1f4f6b" }, "surface": { "base": "#f2f4f5", "panel": "#e6e9ec", "elevated": "#ffffff", "bg1": "#eef1f3", "bg2": "#dde2e6", "bgCard": "#ffffff" }, "ink": { "text": "#1b211d", "muted": 0.76, "dim": 0.66, "faint": 0.42, "line": 0.14, "lineStrong": 0.26, "lineFaint": 0.07 }, "shadows": { "xs": "0 1px 2px rgba(27, 33, 29, 0.07)", ... } } }

Ce qui change entre les thèmes

VariableThème sombreThème clair
--brand#d23a4f#c42a3f
--brand-rgb210, 58, 79196, 42, 63
--tone-primary-on#ffffff#ffffff
--tone-primary-subtlergba(210, 58, 79, 0.12)rgba(196, 42, 63, 0.12)
--tone-primary-on-subtle#dd6b7b#bb283d
--tone-primary-emphasis#d95869#cd4a5c
--tone-secondary-on#0b0b12#ffffff
--ink-text-on-accent#0b0b12#ffffff
--bg-primary#1a211c#f2f4f5
--sf-2#29332c#ffffff
--text-primary#f3f1ea#1b211d
--text-secondaryrgba(243, 241, 234, 0.76)rgba(27, 33, 29, 0.76)
--ui-linergba(243, 241, 234, 0.13)rgba(27, 33, 29, 0.14)
--scrim#141a16#141a16
--scrim-brand#d95869#d95869
Thème sombre de la démo (dark)
--brand#d23a4f
--brand-secondary#e3c49c
--accent#e3c49c
--accent-2#d9a46b
--accent-3#a9b5ad
--accent-soft#3a2328
--success#7fd3a0
--warning#f0c06a
--error#ff8f85
--info#9cc6e0
--bg-primary#1a211c
--bg-secondary#212a24
--bg-tertiary#29332c
--text-primary#f3f1ea
Thème clair de la démo (light, fusionné sur dark)
--brand#c42a3f
--brand-secondary#1f2a24
--accent#1f2a24
--accent-2#8a5a2b
--accent-3#5f6b63
--accent-soft#f6dfe2
--success#2e6b45
--warning#8a5a00
--error#a3231c
--info#1f4f6b
--bg-primary#f2f4f5
--bg-secondary#e6e9ec
--bg-tertiary#ffffff
--text-primary#1b211d

Ce qui est commun aux deux thèmes

VariableValeur
--font-display'Sofia Sans Extra Condensed', ui-sans-serif, -apple-system, system-ui, sans-serif
--text-displayclamp(56px, 4.42vw + 39.43px, 96px)
--leading-display0.95
--text-section-titleclamp(38px, 2.431vw + 28.88px, 60px)
--text-lead, --leading-lead20px, 1.4
--radius-sm, --radius-md, --radius-lg, --radius-xl0px, 2px, 6px, 10px
--radius-btn, --radius-input0px
--space-4, --u16px, 4px
--container-md1180px
--weight-semibold700
  • radius: 6 donne des coins presque droits : --radius-sm tombe à 0, donc boutons et champs sont carrés.
  • Les rôles fluides font grandir les titres avec la page : un titre de section passe de 38 px à 375 px de large à 60 px à 1280 px.
  • Les cinq largeurs de conteneur valent 1180 : à partir de 576 px, .container ne dépasse jamais 1180 px.
  • Le voile est déclaré dans dark et reste le même en clair, comme --scrim-brand, calculé depuis la palette sombre.
  • effects.surface: "soft" fait d'un data-style vide une surface pleine sans filet.
  • Le bloc light n'a ni fonts ni typography : la typographie est la même dans les deux thèmes.
  • weights.semibold vaut 700 : fw-semibold et tout ce qui lit --weight-semibold sont plus gras que d'habitude, sans toucher un bloc.

Règles et pièges

Règles d'usage

  • Jamais de couleur en dur dans un bloc : #c42a3f s'écrit var(--brand), une transparence rgba(var(--brand-rgb), 0.2). Une couleur figée ne suit ni le thème clair ni les changements de charte.
  • Les variables plutôt que les valeurs, aussi pour les tailles, les rayons, les espacements et les durées : var(--text-lg), var(--radius-md), var(--space-4), var(--duration-fast).
  • Les classes d'abord, le styleJson pour ce qu'elles n'expriment pas, le CSS écrit à la main pour le reste : sélecteurs d'enfants, pseudo-éléments complexes, états portés par une classe, images clés.
  • Un bloc ne fixe pas sa largeur de page : la largeur vient de container ou d'une bande frame-*. Un bloc se mesure à son conteneur (@container), pas à la fenêtre ; la vérification des lois du design system signale un max-width de 1000 px ou plus centré, et un @media de largeur dans le CSS d'un bloc.

web/index.css perd contre les blocs

web/index.css n'est pas chargé tel quel : il est inséré, avec le CSS global du framework, dans @layer app-globals. Une règle en couche perd toujours contre une règle hors couche, quelle que soit sa spécificité. Une règle de web/index.css ne corrige donc jamais le style.css d'un bloc, d'un fragment ou d'une mise en page.

  • Bon usage : le socle du document (body, police, box-sizing) et les valeurs de variables propres au projet, comme --shell-clear.
  • Mauvais usage : surcharger un bloc, la règle est ignorée ; poser une remise à zéro large comme *{margin:0;padding:0}, qui bat les couches ds-* déclarées avant app-globals et casse le centrage de .container et les retraits du système.
css
body { background: var(--bg-primary); color: var(--text-primary); font-family: var(--font-sans); line-height: var(--leading-base); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; overflow-x: hidden; }

La mise en page dans layouts/<id>/style.css

Ce qui tient la page autour des blocs (barre collante, zones, filet sous l'en-tête) va dans le style.css de la mise en page, web/configurations/layouts/<id>/style.css. Ce CSS est hors couche et inséré sur chaque page qui monte cette mise en page. Pas dans web/index.css, où ces règles perdraient, ni dans un bloc, qui ne connaît pas la page.

css
.l-site-top { position: sticky; top: 0; z-index: var(--z-sticky); }

Pièges

  • Un spacing incomplet donne undefinedpx pour les clés 0, 1, 2, 3, 4, 6, 8, 12, 16 : marges, retraits et gouttières disparaissent. Même chose pour une opacité de ink manquante.
  • Une couleur écrite autrement qu'en #rrggbb donne un canal -rgb noir (0, 0, 0) et une encre calculée blanche.
  • Sans typography.lineHeights.base, --leading-base n'existe pas, même avec leading.base.
  • radii.radius écrit en chaîne est ignoré : le cadran revient à 14.
  • Une police sans import en https:// dans dark ne se charge pas : le site s'affiche dans la pile de repli.
  • Une variable inventée (--border-color, --font-size-lg) n'existe pas : sans repli, toute la déclaration tombe. Dans un styleJson, elle est refusée à la compilation.
  • Une classe construite à partir d'une donnée est élaguée en ligne alors qu'elle s'affiche dans l'Aperçu.
  • Un sélecteur écrit à la main sur [data-forge-id='<bloc>--<nœud>'] ne trouve en ligne que la racine et les nœuds stylés ; le canevas d'édition marque tous les nœuds et cache l'écart.
  • Redéfinir --brand sur un nœud ne change ni --tone-primary ni --ink-text-on-brand.
  • "defaultTheme": "light" sans bloc light laisse le site sombre.
  • Une valeur de charte qui contient ;, une accolade, < ou un url( hors logo est retirée du <style> sans message.