Chapitre 28 — Design System avec SCSS¶
Introduction¶
Un Design System est une collection de tokens, composants, patterns et guidelines qui garantissent la cohérence visuelle d'un produit. SCSS est l'outil idéal pour construire un design token system : variables, maps, mixins et fonctions permettent de créer un langage visuel unique et maintenable.
Ce chapitre détaille la création d'un système complet de design tokens.
Qu'est-ce qu'un Design Token ?¶
Un design token est la plus petite unité d'un design : une valeur nommée qui représente une décision visuelle. Au lieu d'écrire #3498db partout, on écrit $color-primary ou var(--color-primary).
Types de tokens¶
| Type | Exemple |
|---|---|
| Couleur | $color-primary: #3498db |
| Typographie | $font-size-base: 16px |
| Espacement | $spacing-md: 16px |
| Ombre | $shadow-md: 0 4px 6px rgba(0,0,0,0.1) |
| Bordure | $border-radius-md: 8px |
| Transition | $transition-base: 300ms ease |
| Z-index | $z-modal: 1050 |
1. Palette de couleurs¶
Couleurs de base¶
// tokens/_colors.scss
// === PRIMAIRES ===
$color-blue-50: #eff6ff;
$color-blue-100: #dbeafe;
$color-blue-200: #bfdbfe;
$color-blue-300: #93c5fd;
$color-blue-400: #60a5fa;
$color-blue-500: #3b82f6; // Primary
$color-blue-600: #2563eb;
$color-blue-700: #1d4ed8;
$color-blue-800: #1e40af;
$color-blue-900: #1e3a8a;
$color-primary: $color-blue-500;
$color-primary-light: $color-blue-300;
$color-primary-dark: $color-blue-700;
// === SECONDAIRES ===
$color-green-50: #f0fdf4;
$color-green-100: #dcfce7;
$color-green-200: #bbf7d0;
$color-green-300: #86efac;
$color-green-400: #4ade80;
$color-green-500: #22c55e; // Secondary
$color-green-600: #16a34a;
$color-green-700: #15803d;
$color-green-800: #166534;
$color-green-900: #14532d;
$color-secondary: $color-green-500;
$color-secondary-light: $color-green-300;
$color-secondary-dark: $color-green-700;
// === DANGER / ERREUR ===
$color-red-50: #fef2f2;
$color-red-100: #fee2e2;
$color-red-200: #fecaca;
$color-red-300: #fca5a5;
$color-red-400: #f87171;
$color-red-500: #ef4444; // Danger
$color-red-600: #dc2626;
$color-red-700: #b91c1c;
$color-red-800: #991b1b;
$color-red-900: #7f1d1d;
$color-danger: $color-red-500;
$color-danger-light: $color-red-300;
$color-danger-dark: $color-red-700;
// === AVERTISSEMENT ===
$color-amber-50: #fffbeb;
$color-amber-100: #fef3c7;
$color-amber-200: #fde68a;
$color-amber-300: #fcd34d;
$color-amber-400: #fbbf24;
$color-amber-500: #f59e0b; // Warning
$color-amber-600: #d97706;
$color-amber-700: #b45309;
$color-amber-800: #92400e;
$color-amber-900: #78350f;
$color-warning: $color-amber-500;
$color-warning-light: $color-amber-300;
$color-warning-dark: $color-amber-700;
// === NEUTRES ===
$color-gray-50: #f9fafb;
$color-gray-100: #f3f4f6;
$color-gray-200: #e5e7eb;
$color-gray-300: #d1d5db;
$color-gray-400: #9ca3af;
$color-gray-500: #6b7280;
$color-gray-600: #4b5563;
$color-gray-700: #374151;
$color-gray-800: #1f2937;
$color-gray-900: #111827;
// === SEMANTIQUES ===
$color-text: $color-gray-900;
$color-text-secondary: $color-gray-600;
$color-text-muted: $color-gray-400;
$color-text-inverse: #ffffff;
$color-bg: #ffffff;
$color-bg-secondary: $color-gray-50;
$color-bg-tertiary: $color-gray-100;
$color-border: $color-gray-200;
$color-border-hover: $color-gray-300;
$color-border-focus: $color-primary;
Couleurs en tant que map¶
// tokens/_color-map.scss
$colors: (
// Primaires
'blue': (
'50': $color-blue-50,
'100': $color-blue-100,
'200': $color-blue-200,
'300': $color-blue-300,
'400': $color-blue-400,
'500': $color-blue-500,
'600': $color-blue-600,
'700': $color-blue-700,
'800': $color-blue-800,
'900': $color-blue-900,
),
'green': (
'50': $color-green-50,
'100': $color-green-100,
'200': $color-green-200,
'300': $color-green-300,
'400': $color-green-400,
'500': $color-green-500,
'600': $color-green-600,
'700': $color-green-700,
'800': $color-green-800,
'900': $color-green-900,
),
'red': (
'50': $color-red-50,
'100': $color-red-100,
'200': $color-red-200,
'300': $color-red-300,
'400': $color-red-400,
'500': $color-red-500,
'600': $color-red-600,
'700': $color-red-700,
'800': $color-red-800,
'900': $color-red-900,
),
'amber': (
'50': $color-amber-50,
'100': $color-amber-100,
'200': $color-amber-200,
'300': $color-amber-300,
'400': $color-amber-400,
'500': $color-amber-500,
'600': $color-amber-600,
'700': $color-amber-700,
'800': $color-amber-800,
'900': $color-amber-900,
),
'gray': (
'50': $color-gray-50,
'100': $color-gray-100,
'200': $color-gray-200,
'300': $color-gray-300,
'400': $color-gray-400,
'500': $color-gray-500,
'600': $color-gray-600,
'700': $color-gray-700,
'800': $color-gray-800,
'900': $color-gray-900,
),
);
// Fonction pour récupérer une couleur
@function color($family, $shade) {
@return map-deep-get($colors, $family, $shade);
}
2. Échelle d'espacement (Spacing Scale)¶
Un système d'espacement basé sur une unité de base (4px ou 8px) crée de la cohérence visuelle.
// tokens/_spacing.scss
// Système basé sur 4px
$spacing-unit: 4px;
$spacing-0: 0;
$spacing-px: 1px;
$spacing-0-5: $spacing-unit * 0.5; // 2px
$spacing-1: $spacing-unit * 1; // 4px
$spacing-1-5: $spacing-unit * 1.5; // 6px
$spacing-2: $spacing-unit * 2; // 8px
$spacing-2-5: $spacing-unit * 2.5; // 10px
$spacing-3: $spacing-unit * 3; // 12px
$spacing-4: $spacing-unit * 4; // 16px
$spacing-5: $spacing-unit * 5; // 20px
$spacing-6: $spacing-unit * 6; // 24px
$spacing-7: $spacing-unit * 7; // 28px
$spacing-8: $spacing-unit * 8; // 32px
$spacing-9: $spacing-unit * 9; // 36px
$spacing-10: $spacing-unit * 10; // 40px
$spacing-12: $spacing-unit * 12; // 48px
$spacing-14: $spacing-unit * 14; // 56px
$spacing-16: $spacing-unit * 16; // 64px
$spacing-20: $spacing-unit * 20; // 80px
$spacing-24: $spacing-unit * 24; // 96px
$spacing-28: $spacing-unit * 28; // 112px
$spacing-32: $spacing-unit * 32; // 128px
// Map des espacements
$spacing-map: (
0: $spacing-0,
px: $spacing-px,
0.5: $spacing-0-5,
1: $spacing-1,
1.5: $spacing-1-5,
2: $spacing-2,
2.5: $spacing-2-5,
3: $spacing-3,
4: $spacing-4,
5: $spacing-5,
6: $spacing-6,
7: $spacing-7,
8: $spacing-8,
9: $spacing-9,
10: $spacing-10,
12: $spacing-12,
14: $spacing-14,
16: $spacing-16,
20: $spacing-20,
24: $spacing-24,
28: $spacing-28,
32: $spacing-32,
);
// Fonction utilitaire
@function space($size) {
@return map-get($spacing-map, $size);
}
// Map pour les gaps
$gap-map: (
none: 0,
xs: $spacing-1,
sm: $spacing-2,
md: $spacing-4,
lg: $spacing-6,
xl: $spacing-8,
2xl: $spacing-12,
);
Génération automatique de classes utilitaires¶
// utilities/_spacing-generated.scss
@each $size-name, $size-value in $spacing-map {
.m-#{$size-name} { margin: $size-value !important; }
.mt-#{$size-name} { margin-top: $size-value !important; }
.mr-#{$size-name} { margin-right: $size-value !important; }
.mb-#{$size-name} { margin-bottom: $size-value !important; }
.ml-#{$size-name} { margin-left: $size-value !important; }
.mx-#{$size-name} {
margin-left: $size-value !important;
margin-right: $size-value !important;
}
.my-#{$size-name} {
margin-top: $size-value !important;
margin-bottom: $size-value !important;
}
.p-#{$size-name} { padding: $size-value !important; }
.pt-#{$size-name} { padding-top: $size-value !important; }
.pr-#{$size-name} { padding-right: $size-value !important; }
.pb-#{$size-name} { padding-bottom: $size-value !important; }
.pl-#{$size-name} { padding-left: $size-value !important; }
.px-#{$size-name} {
padding-left: $size-value !important;
padding-right: $size-value !important;
}
.py-#{$size-name} {
padding-top: $size-value !important;
padding-bottom: $size-value !important;
}
}
.mx-auto { margin-left: auto !important; margin-right: auto !important; }
.ml-auto { margin-left: auto !important; }
.mr-auto { margin-right: auto !important; }
3. Échelle typographique¶
Système typographique complet¶
// tokens/_typography.scss
// Polices
$font-sans: (
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif,
);
$font-serif: (
'Merriweather',
Georgia,
'Times New Roman',
serif,
);
$font-mono: (
'JetBrains Mono',
'Fira Code',
'Courier New',
monospace,
);
// Tailles de police — échelle modulaire (ratio 1.25)
$font-size-base: 16px;
$font-size-xs: calc(#{$font-size-base} * 0.75); // 12px
$font-size-sm: calc(#{$font-size-base} * 0.875); // 14px
$font-size-md: $font-size-base; // 16px
$font-size-lg: calc(#{$font-size-base} * 1.125); // 18px
$font-size-xl: calc(#{$font-size-base} * 1.25); // 20px
$font-size-2xl: calc(#{$font-size-base} * 1.5); // 24px
$font-size-3xl: calc(#{$font-size-base} * 1.875); // 30px
$font-size-4xl: calc(#{$font-size-base} * 2.25); // 36px
$font-size-5xl: calc(#{$font-size-base} * 3); // 48px
$font-size-6xl: calc(#{$font-size-base} * 3.75); // 60px
// Map des tailles
$font-size-map: (
xs: $font-size-xs,
sm: $font-size-sm,
base: $font-size-md,
lg: $font-size-lg,
xl: $font-size-xl,
'2xl': $font-size-2xl,
'3xl': $font-size-3xl,
'4xl': $font-size-4xl,
'5xl': $font-size-5xl,
'6xl': $font-size-6xl,
);
// Poids de police
$font-weight-thin: 100;
$font-weight-extralight: 200;
$font-weight-light: 300;
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-semibold: 600;
$font-weight-bold: 700;
$font-weight-extrabold: 800;
$font-weight-black: 900;
$font-weight-map: (
thin: $font-weight-thin,
extralight: $font-weight-extralight,
light: $font-weight-light,
normal: $font-weight-normal,
medium: $font-weight-medium,
semibold: $font-weight-semibold,
bold: $font-weight-bold,
extrabold: $font-weight-extrabold,
black: $font-weight-black,
);
// Interlignes
$line-height-none: 1;
$line-height-tight: 1.25;
$line-height-snug: 1.375;
$line-height-normal: 1.5;
$line-height-relaxed: 1.625;
$line-height-loose: 2;
$line-height-map: (
none: $line-height-none,
tight: $line-height-tight,
snug: $line-height-snug,
normal: $line-height-normal,
relaxed: $line-height-relaxed,
loose: $line-height-loose,
);
// Interlettres
$tracking-tighter: -0.05em;
$tracking-tight: -0.025em;
$tracking-normal: 0em;
$tracking-wide: 0.025em;
$tracking-wider: 0.05em;
$tracking-widest: 0.1em;
$tracking-map: (
tighter: $tracking-tighter,
tight: $tracking-tight,
normal: $tracking-normal,
wide: $tracking-wide,
wider: $tracking-wider,
widest: $tracking-widest,
);
Fonctions typographiques¶
// tokens/_typography-functions.scss
@function font-size($size) {
@return map-get($font-size-map, $size);
}
@function weight($weight) {
@return map-get($font-weight-map, $weight);
}
@function leading($line-height) {
@return map-get($line-height-map, $line-height);
}
@function tracking($tracking) {
@return map-get($tracking-map, $tracking);
}
Classes utilitaires typographiques générées¶
// utilities/_typography-generated.scss
// Tailles
@each $name, $value in $font-size-map {
.text-#{$name} {
font-size: $value;
}
}
// Poids
@each $name, $value in $font-weight-map {
.font-#{$name} {
font-weight: $value;
}
}
// Interlignes
@each $name, $value in $line-height-map {
.leading-#{$name} {
line-height: $value;
}
}
// Interlettres
@each $name, $value in $tracking-map {
.tracking-#{$name} {
letter-spacing: $value;
}
}
// Familles
.font-sans { font-family: $font-sans; }
.font-serif { font-family: $font-serif; }
.font-mono { font-family: $font-mono; }
4. Border Radius¶
// tokens/_border-radius.scss
$border-radius-none: 0;
$border-radius-sm: 2px;
$border-radius-md: 6px;
$border-radius-lg: 8px;
$border-radius-xl: 12px;
$border-radius-2xl: 16px;
$border-radius-3xl: 24px;
$border-radius-full: 9999px;
$border-radius-map: (
none: $border-radius-none,
sm: $border-radius-sm,
md: $border-radius-md,
lg: $border-radius-lg,
xl: $border-radius-xl,
'2xl': $border-radius-2xl,
'3xl': $border-radius-3xl,
full: $border-radius-full,
);
// Classes générées
@each $name, $value in $border-radius-map {
.rounded-#{$name} {
border-radius: $value !important;
}
}
// Bordures
$border-width-thin: 1px;
$border-width-base: 2px;
$border-width-thick: 4px;
$border-color: $color-gray-200;
$border-color-light: $color-gray-100;
$border-color-dark: $color-gray-400;
$border-map: (
thin: $border-width-thin,
base: $border-width-base,
thick: $border-width-thick,
);
5. Ombres (Shadows)¶
// tokens/_shadows.scss
// Échelle d'ombres
$shadow-none: none;
$shadow-xs: (
0 1px 2px 0 rgba(0, 0, 0, 0.05),
);
$shadow-sm: (
0 1px 3px 0 rgba(0, 0, 0, 0.1),
0 1px 2px -1px rgba(0, 0, 0, 0.1),
);
$shadow-md: (
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1),
);
$shadow-lg: (
0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -4px rgba(0, 0, 0, 0.1),
);
$shadow-xl: (
0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1),
);
$shadow-2xl: (
0 25px 50px -12px rgba(0, 0, 0, 0.25),
);
$shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05);
// Ombres colorées
$shadow-primary: 0 4px 14px 0 rgba($color-primary, 0.39);
$shadow-danger: 0 4px 14px 0 rgba($color-danger, 0.39);
$shadow-success: 0 4px 14px 0 rgba($color-secondary, 0.39);
// Map
$shadow-map: (
none: $shadow-none,
xs: $shadow-xs,
sm: $shadow-sm,
md: $shadow-md,
lg: $shadow-lg,
xl: $shadow-xl,
'2xl': $shadow-2xl,
inner: $shadow-inner,
primary: $shadow-primary,
danger: $shadow-danger,
success: $shadow-success,
);
// Fonction
@function shadow($name) {
@return map-get($shadow-map, $name);
}
// Classes générées
@each $name, $value in $shadow-map {
.shadow-#{$name} {
box-shadow: $value !important;
}
}
6. Transitions¶
// tokens/_transitions.scss
// Durées
$duration-instant: 0ms;
$duration-fast: 100ms;
$duration-normal: 200ms;
$duration-base: 300ms;
$duration-slow: 500ms;
$duration-slower: 700ms;
$duration-map: (
instant: $duration-instant,
fast: $duration-fast,
normal: $duration-normal,
base: $duration-base,
slow: $duration-slow,
slower: $duration-slower,
);
// Courbes d'accélération
$ease-linear: linear;
$ease-in: cubic-bezier(0.4, 0, 1, 1);
$ease-out: cubic-bezier(0, 0, 0.2, 1);
$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
$ease-bounce: cubic-bezier(0.68, -0.55, 0.27, 1.55);
$ease-elastic: cubic-bezier(0.175, 0.885, 0.32, 1.275);
$ease-map: (
linear: $ease-linear,
in: $ease-in,
out: $ease-out,
'in-out': $ease-in-out,
bounce: $ease-bounce,
elastic: $ease-elastic,
);
// Properties
$transition-colors: color, background-color, border-color, box-shadow;
$transition-transform: transform;
$transition-opacity: opacity;
$transition-all: all;
// Mixin de transition
@mixin transition($properties: all, $duration: $duration-base, $easing: $ease-in-out) {
transition-property: $properties;
transition-duration: $duration;
transition-timing-function: $easing;
}
// Mixin de transition personnalisée
@mixin transition-map($map) {
$result: '';
@each $property, $value in $map {
@if $result != '' {
$result: $result + ',';
}
@if map-has-key($duration-map, map-get($value, duration)) {
$value: map-merge($value, (duration: map-get($duration-map, map-get($value, duration))));
}
@if map-has-key($ease-map, map-get($value, ease)) {
$value: map-merge($value, (ease: map-get($ease-map, map-get($value, ease))));
}
$result: #{$result} #{$property} #{map-get($value, duration) $duration-base} #{map-get($value, ease) $ease-in-out};
}
transition: #{$result};
}
Utilisation pratique¶
// Composant utilisant les tokens de transition
.card {
@include transition($transition-colors, $duration-base, $ease-in-out);
&:hover {
box-shadow: shadow(lg);
}
}
.btn {
@include transition(transform, $duration-fast, $ease-out);
&:active {
transform: scale(0.97);
}
}
.modal {
@include transition(opacity, $duration-normal, $ease-in-out);
}
Système de thèmes (Dark/Light)¶
// tokens/_themes.scss
:root {
// Couleurs
--color-primary: #{$color-primary};
--color-primary-light: #{$color-primary-light};
--color-primary-dark: #{$color-primary-dark};
--color-secondary: #{$color-secondary};
--color-danger: #{$color-danger};
--color-warning: #{$color-warning};
--color-text: #{$color-text};
--color-text-secondary: #{$color-text-secondary};
--color-text-muted: #{$color-text-muted};
--color-text-inverse: #{$color-text-inverse};
--color-bg: #{$color-bg};
--color-bg-secondary: #{$color-bg-secondary};
--color-bg-tertiary: #{$color-bg-tertiary};
--color-border: #{$color-border};
--color-border-hover: #{$color-border-hover};
--color-border-focus: #{$color-border-focus};
// Ombres
--shadow-sm: #{$shadow-sm};
--shadow-md: #{$shadow-md};
--shadow-lg: #{$shadow-lg};
--shadow-xl: #{$shadow-xl};
// Transitions
--transition-fast: #{$duration-fast}ms #{$ease-in-out};
--transition-base: #{$duration-base}ms #{$ease-in-out};
--transition-slow: #{$duration-slow}ms #{$ease-in-out};
}
[data-theme="dark"],
.theme-dark {
--color-primary: #{lighten($color-primary, 10%)};
--color-primary-light: #{lighten($color-primary, 25%)};
--color-primary-dark: #{$color-primary};
--color-secondary: #{lighten($color-secondary, 10%)};
--color-danger: #{lighten($color-danger, 10%)};
--color-warning: #{lighten($color-warning, 10%)};
--color-text: #e2e8f0;
--color-text-secondary: #a0aec0;
--color-text-muted: #718096;
--color-text-inverse: #1a202c;
--color-bg: #1a202c;
--color-bg-secondary: #2d3748;
--color-bg-tertiary: #4a5568;
--color-border: #4a5568;
--color-border-hover: #718096;
--color-border-focus: var(--color-primary);
--shadow-sm: 0 1px 3px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
--shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.4);
}
// Thème entreprise personnalisé
.theme-enterprise {
--color-primary: #003366;
--color-primary-light: #004488;
--color-primary-dark: #002244;
--color-secondary: #0066cc;
--color-danger: #cc0000;
--font-sans: 'Roboto', sans-serif;
}
Arbre du Design System¶
design-system/
├── tokens/
│ ├── _colors.scss
│ ├── _color-map.scss
│ ├── _spacing.scss
│ ├── _typography.scss
│ ├── _typography-functions.scss
│ ├── _border-radius.scss
│ ├── _shadows.scss
│ ├── _transitions.scss
│ └── _themes.scss
│
├── mixins/
│ ├── _responsive.scss
│ ├── _layout.scss
│ ├── _typography.scss
│ └── _animation.scss
│
├── utilities/
│ ├── _spacing-generated.scss
│ ├── _typography-generated.scss
│ ├── _display-generated.scss
│ ├── _shadow-generated.scss
│ └── _radius-generated.scss
│
└── main.scss
Intégration CSS Custom Properties¶
Pour un design system moderne, on peut combiner SCSS pour la compilation et CSS Custom Properties pour le runtime :
// tokens/_runtime.scss
// Génère des custom properties à partir des tokens SCSS
@mixin generate-custom-properties($namespace) {
@each $key, $value in $spacing-map {
--#{$namespace}-space-#{$key}: #{$value};
}
@each $key, $value in $font-size-map {
--#{$namespace}-font-size-#{$key}: #{$value};
}
@each $key, $value in $shadow-map {
--#{$namespace}-shadow-#{$key}: #{$value};
}
@each $key, $value in $border-radius-map {
--#{$namespace}-radius-#{$key}: #{$value};
}
}
// Utilisation
:root {
@include generate-custom-properties('ds');
}
// Résultat en CSS :
// :root {
// --ds-space-0: 0;
// --ds-space-1: 4px;
// --ds-space-2: 8px;
// --ds-space-4: 16px;
// --ds-font-size-sm: 14px;
// --ds-font-size-base: 16px;
// --ds-shadow-md: 0 4px 6px -1px rgba(0,0,0,0.1);
// --ds-radius-md: 6px;
// ...
// }
Résumé¶
Un design token system en SCSS crée une source unique de vérité pour toutes les valeurs visuelles d'un projet. En combinant des variables, des maps, des fonctions et des mixins, on obtient un système cohérent, maintenable et évolutif. Les classes générées automatiquement assurent une utilisation rapide en HTML tout en garantissant la cohérence visuelle.