Chapitre 36 — Projet Final : Mini Design System¶
Introduction¶
Ce projet final synthétise toutes les notions apprises au cours des 35 chapitres précédents. Nous allons construire un Mini Design System complet en SCSS, en utilisant l'architecture 7-1, les @use/@forward, un système de tokens, des composants, des utilities, et des thèmes light/dark.
Objectifs du projet¶
- Architecture 7-1 complète avec
@useet@forward - Palette de couleurs avec variables et maps
- Échelle d'espacement
- Échelle typographique
- Système de boutons
- Composant Cartes
- Système de formulaires
- Grille responsive
- Classes utilitaires auto-générées
- Thèmes Light/Dark
Arbre du projet¶
mini-design-system/
├── src/
│ ├── abstracts/
│ │ ├── _index.scss
│ │ ├── _variables.scss
│ │ ├── _colors.scss
│ │ ├── _typography.scss
│ │ ├── _spacing.scss
│ │ ├── _breakpoints.scss
│ │ ├── _shadows.scss
│ │ ├── _borders.scss
│ │ ├── _transitions.scss
│ │ ├── _z-index.scss
│ │ ├── _functions.scss
│ │ └── _mixins.scss
│ │
│ ├── base/
│ │ ├── _index.scss
│ │ ├── _reset.scss
│ │ ├── _typography.scss
│ │ └── _global.scss
│ │
│ ├── components/
│ │ ├── _index.scss
│ │ ├── _buttons.scss
│ │ ├── _cards.scss
│ │ ├── _forms.scss
│ │ ├── _badges.scss
│ │ └── _alerts.scss
│ │
│ ├── layout/
│ │ ├── _index.scss
│ │ ├── _grid.scss
│ │ ├── _container.scss
│ │ ├── _header.scss
│ │ └── _footer.scss
│ │
│ ├── themes/
│ │ ├── _index.scss
│ │ ├── _light.scss
│ │ └── _dark.scss
│ │
│ ├── utilities/
│ │ ├── _index.scss
│ │ ├── _display.scss
│ │ ├── _text.scss
│ │ ├── _spacing.scss
│ │ └── _generated.scss
│ │
│ └── main.scss
│
├── dist/
│ └── css/
│ └── main.css
│
├── index.html
├── package.json
└── README.md
Fichier d'entrée principal¶
// src/main.scss
// ============================================================
// MINI DESIGN SYSTEM
// Architecture 7-1 avec @use et @forward
// ============================================================
// 1. ABSTRACTS (aucun CSS généré)
@use 'abstracts';
// 2. BASE (styles globaux)
@use 'base';
// 3. LAYOUT (structure)
@use 'layout';
// 4. COMPONENTS (composants UI)
@use 'components';
// 5. THEMES (thèmes visuels)
@use 'themes';
// 6. UTILITIES (classes d'aide)
@use 'utilities';
1. Abstracts — Le système de tokens¶
_index.scss¶
// src/abstracts/_index.scss
@forward 'variables';
@forward 'colors';
@forward 'typography';
@forward 'spacing';
@forward 'breakpoints';
@forward 'shadows';
@forward 'borders';
@forward 'transitions';
@forward 'z-index';
@forward 'functions';
@forward 'mixins';
_variables.scss¶
// src/abstracts/_variables.scss
// ============================================================
// VARIABLES GÉNÉRALES
// ============================================================
// Conteneur
$container-max-width: 1200px !default;
$container-padding: 16px !default;
// Z-index base
$z-dropdown: 1000 !default;
$z-sticky: 1020 !default;
$z-fixed: 1030 !default;
$z-modal-backdrop: 1040 !default;
$z-modal: 1050 !default;
$z-tooltip: 1060 !default;
_colors.scss¶
// src/abstracts/_colors.scss
// ============================================================
// PALETTE DE COULEURS
// ============================================================
// --- Bleu (Primary) ---
$blue-50: #eff6ff !default;
$blue-100: #dbeafe !default;
$blue-200: #bfdbfe !default;
$blue-300: #93c5fd !default;
$blue-400: #60a5fa !default;
$blue-500: #3b82f6 !default;
$blue-600: #2563eb !default;
$blue-700: #1d4ed8 !default;
$blue-800: #1e40af !default;
$blue-900: #1e3a8a !default;
// --- Vert (Secondary) ---
$green-50: #f0fdf4 !default;
$green-100: #dcfce7 !default;
$green-200: #bbf7d0 !default;
$green-300: #86efac !default;
$green-400: #4ade80 !default;
$green-500: #22c55e !default;
$green-600: #16a34a !default;
$green-700: #15803d !default;
$green-800: #166534 !default;
$green-900: #14532d !default;
// --- Rouge (Danger) ---
$red-50: #fef2f2 !default;
$red-100: #fee2e2 !default;
$red-200: #fecaca !default;
$red-300: #fca5a5 !default;
$red-400: #f87171 !default;
$red-500: #ef4444 !default;
$red-600: #dc2626 !default;
$red-700: #b91c1c !default;
$red-800: #991b1b !default;
$red-900: #7f1d1d !default;
// --- Orange (Warning) ---
$orange-50: #fff7ed !default;
$orange-100: #ffedd5 !default;
$orange-200: #fed7aa !default;
$orange-300: #fdba74 !default;
$orange-400: #fb923c !default;
$orange-500: #f97316 !default;
$orange-600: #ea580c !default;
$orange-700: #c2410c !default;
$orange-800: #9a3412 !default;
$orange-900: #7c2d12 !default;
// --- Gris (Neutres) ---
$gray-50: #f9fafb !default;
$gray-100: #f3f4f6 !default;
$gray-200: #e5e7eb !default;
$gray-300: #d1d5db !default;
$gray-400: #9ca3af !default;
$gray-500: #6b7280 !default;
$gray-600: #4b5563 !default;
$gray-700: #374151 !default;
$gray-800: #1f2937 !default;
$gray-900: #111827 !default;
// --- Couleurs sémantiques ---
$color-primary: $blue-500 !default;
$color-primary-light: $blue-300 !default;
$color-primary-dark: $blue-700 !default;
$color-secondary: $green-500 !default;
$color-secondary-light: $green-300 !default;
$color-secondary-dark: $green-700 !default;
$color-danger: $red-500 !default;
$color-danger-light: $red-300 !default;
$color-danger-dark: $red-700 !default;
$color-warning: $orange-500 !default;
$color-warning-light: $orange-300 !default;
$color-warning-dark: $orange-700 !default;
$color-success: $green-500 !default;
$color-success-light: $green-300 !default;
$color-success-dark: $green-700 !default;
// --- Neutres sémantiques ---
$color-text: $gray-900 !default;
$color-text-secondary: $gray-600 !default;
$color-text-muted: $gray-400 !default;
$color-text-inverse: #ffffff !default;
$color-bg: #ffffff !default;
$color-bg-secondary: $gray-50 !default;
$color-bg-tertiary: $gray-100 !default;
$color-border: $gray-200 !default;
$color-border-hover: $gray-300 !default;
$color-border-focus: $color-primary !default;
// --- Map complète des couleurs ---
$colors: (
'primary': $color-primary,
'primary-light': $color-primary-light,
'primary-dark': $color-primary-dark,
'secondary': $color-secondary,
'danger': $color-danger,
'warning': $color-warning,
'success': $color-success,
'text': $color-text,
'text-secondary': $color-text-secondary,
'text-muted': $color-text-muted,
'bg': $color-bg,
'bg-secondary': $color-bg-secondary,
'border': $color-border,
) !default;
_typography.scss¶
// src/abstracts/_typography.scss
// ============================================================
// TYPOGRAPHIE
// ============================================================
// --- Polices ---
$font-sans: (
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
sans-serif,
) !default;
$font-mono: (
'JetBrains Mono',
'Fira Code',
'Courier New',
monospace,
) !default;
// --- Tailles ---
$font-size-xs: 0.75rem !default; // 12px
$font-size-sm: 0.875rem !default; // 14px
$font-size-base: 1rem !default; // 16px
$font-size-lg: 1.125rem !default; // 18px
$font-size-xl: 1.25rem !default; // 20px
$font-size-2xl: 1.5rem !default; // 24px
$font-size-3xl: 1.875rem !default; // 30px
$font-size-4xl: 2.25rem !default; // 36px
$font-size-5xl: 3rem !default; // 48px
$font-size-map: (
xs: $font-size-xs,
sm: $font-size-sm,
base: $font-size-base,
lg: $font-size-lg,
xl: $font-size-xl,
'2xl': $font-size-2xl,
'3xl': $font-size-3xl,
'4xl': $font-size-4xl,
'5xl': $font-size-5xl,
) !default;
// --- Poids ---
$font-weight-light: 300 !default;
$font-weight-normal: 400 !default;
$font-weight-medium: 500 !default;
$font-weight-semibold: 600 !default;
$font-weight-bold: 700 !default;
// --- Interlignes ---
$line-height-tight: 1.25 !default;
$line-height-snug: 1.375 !default;
$line-height-normal: 1.5 !default;
$line-height-relaxed: 1.625 !default;
$line-height-loose: 2 !default;
_spacing.scss¶
// src/abstracts/_spacing.scss
// ============================================================
// ESPACEMENT
// ============================================================
$spacing-unit: 4px !default;
$spacing-0: 0 !default;
$spacing-px: 1px !default;
$spacing-0-5: $spacing-unit * 0.5 !default; // 2px
$spacing-1: $spacing-unit !default; // 4px
$spacing-1-5: $spacing-unit * 1.5 !default; // 6px
$spacing-2: $spacing-unit * 2 !default; // 8px
$spacing-3: $spacing-unit * 3 !default; // 12px
$spacing-4: $spacing-unit * 4 !default; // 16px
$spacing-5: $spacing-unit * 5 !default; // 20px
$spacing-6: $spacing-unit * 6 !default; // 24px
$spacing-8: $spacing-unit * 8 !default; // 32px
$spacing-10: $spacing-unit * 10 !default; // 40px
$spacing-12: $spacing-unit * 12 !default; // 48px
$spacing-16: $spacing-unit * 16 !default; // 64px
$spacing-20: $spacing-unit * 20 !default; // 80px
$spacing-map: (
0: $spacing-0,
px: $spacing-px,
0.5: $spacing-0-5,
1: $spacing-1,
1.5: $spacing-1-5,
2: $spacing-2,
3: $spacing-3,
4: $spacing-4,
5: $spacing-5,
6: $spacing-6,
8: $spacing-8,
10: $spacing-10,
12: $spacing-12,
16: $spacing-16,
20: $spacing-20,
) !default;
_breakpoints.scss¶
// src/abstracts/_breakpoints.scss
// ============================================================
// BREAKPOINTS
// ============================================================
$breakpoint-sm: 576px !default;
$breakpoint-md: 768px !default;
$breakpoint-lg: 992px !default;
$breakpoint-xl: 1200px !default;
$breakpoint-2xl: 1400px !default;
$breakpoints: (
sm: $breakpoint-sm,
md: $breakpoint-md,
lg: $breakpoint-lg,
xl: $breakpoint-xl,
'2xl': $breakpoint-2xl,
) !default;
_shadows.scss¶
// src/abstracts/_shadows.scss
// ============================================================
// OMBRES
// ============================================================
$shadow-none: none !default;
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05) !default;
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1) !default;
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1) !default;
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !default;
$shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25) !default;
$shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05) !default;
$shadow-map: (
none: $shadow-none,
sm: $shadow-sm,
md: $shadow-md,
lg: $shadow-lg,
xl: $shadow-xl,
'2xl': $shadow-2xl,
inner: $shadow-inner,
) !default;
_borders.scss¶
// src/abstracts/_borders.scss
// ============================================================
// BORDURES
// ============================================================
$border-width: 1px !default;
$border-color: $color-border !default;
$border-radius-none: 0 !default;
$border-radius-sm: 0.25rem !default; // 4px
$border-radius-md: 0.5rem !default; // 8px
$border-radius-lg: 0.75rem !default; // 12px
$border-radius-xl: 1rem !default; // 16px
$border-radius-full: 9999px !default;
$border-radius-map: (
none: $border-radius-none,
sm: $border-radius-sm,
md: $border-radius-md,
lg: $border-radius-lg,
xl: $border-radius-xl,
full: $border-radius-full,
) !default;
_transitions.scss¶
// src/abstracts/_transitions.scss
// ============================================================
// TRANSITIONS
// ============================================================
$duration-fast: 150ms !default;
$duration-normal: 300ms !default;
$duration-slow: 500ms !default;
$ease-default: cubic-bezier(0.4, 0, 0.2, 1) !default;
$ease-in: cubic-bezier(0.4, 0, 1, 1) !default;
$ease-out: cubic-bezier(0, 0, 0.2, 1) !default;
$ease-in-out: cubic-bezier(0.4, 0, 0.2, 1) !default;
$ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55) !default;
$transition-base: $duration-normal $ease-default !default;
$transition-fast: $duration-fast $ease-default !default;
$transition-slow: $duration-slow $ease-default !default;
_z-index.scss¶
// src/abstracts/_z-index.scss
// ============================================================
// Z-INDEX
// ============================================================
$z-dropdown: 1000 !default;
$z-sticky: 1020 !default;
$z-fixed: 1030 !default;
$z-modal-backdrop: 1040 !default;
$z-modal: 1050 !default;
$z-tooltip: 1060 !default;
$z-map: (
dropdown: $z-dropdown,
sticky: $z-sticky,
fixed: $z-fixed,
modal-backdrop: $z-modal-backdrop,
modal: $z-modal,
tooltip: $z-tooltip,
) !default;
_functions.scss¶
// src/abstracts/_functions.scss
// ============================================================
// FONCTIONS
// ============================================================
@use 'colors' as *;
// Récupérer une couleur de la map
@function color($name) {
@if map-has-key($colors, $name) {
@return map-get($colors, $name);
}
@warn "Couleur `#{$name}` introuvable.";
@return null;
}
// Récupérer un espace
@function space($size) {
@if map-has-key($spacing-map, $size) {
@return map-get($spacing-map, $size);
}
@warn "Taille `#{$size}` introuvable.";
@return null;
}
// Récupérer une taille de police
@function font-size($size) {
@if map-has-key($font-size-map, $size) {
@return map-get($font-size-map, $size);
}
@warn "Taille `#{$size}` introuvable.";
@return null;
}
// Récupérer une ombre
@function shadow($name) {
@if map-has-key($shadow-map, $name) {
@return map-get($shadow-map, $name);
}
@warn "Ombre `#{$name}` introuvable.";
@return null;
}
// Récupérer un border-radius
@function radius($name) {
@if map-has-key($border-radius-map, $name) {
@return map-get($border-radius-map, $name);
}
@warn "Border radius `#{$name}` introuvable.";
@return null;
}
// Récupérer un z-index
@function z($layer) {
@if map-has-key($z-map, $layer) {
@return map-get($z-map, $layer);
}
@warn "Couche z-index `#{$layer}` introuvable.";
@return null;
}
_mixins.scss¶
// src/abstracts/_mixins.scss
// ============================================================
// MIXINS
// ============================================================
@use 'breakpoints' as *;
// --- Responsive ---
@mixin respond-to($breakpoint) {
$value: map-get($breakpoints, $breakpoint);
@if $value {
@media (min-width: $value) {
@content;
}
} @else {
@warn "Breakpoint `#{$breakpoint}` introuvable.";
}
}
@mixin respond-below($breakpoint) {
$value: map-get($breakpoints, $breakpoint);
@if $value {
@media (max-width: ($value - 0.02px)) {
@content;
}
}
}
// --- Layout ---
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
@mixin flex-column {
display: flex;
flex-direction: column;
}
// --- Accessibilité ---
@mixin visually-hidden {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
// --- Texte ---
@mixin truncate($max-width: 100%) {
max-width: $max-width;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@mixin line-clamp($lines: 2) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
// --- Transitions ---
@mixin transition($properties: all, $duration: $duration-normal, $easing: $ease-default) {
transition-property: $properties;
transition-duration: $duration;
transition-timing-function: $easing;
}
@mixin transition-colors {
@include transition(color, background-color, border-color, box-shadow);
}
// --- Taille ---
@mixin size($width, $height: $width) {
width: $width;
height: $height;
}
@mixin square($size) {
@include size($size);
}
// --- Position ---
@mixin absolute-fill {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
// --- Scrollbar ---
@mixin scrollbar($width: 6px, $thumb: $gray-300) {
&::-webkit-scrollbar {
width: $width;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: $thumb;
border-radius: $border-radius-full;
}
}
2. Base — Styles globaux¶
_index.scss¶
// src/base/_index.scss
@use '../abstracts' as *;
@forward 'reset';
@forward 'typography';
@forward 'global';
_reset.scss¶
// src/base/_reset.scss
@use '../abstracts' as *;
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 100%;
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
a {
color: inherit;
text-decoration: none;
}
ul, ol {
list-style: none;
padding: 0;
margin: 0;
}
button {
cursor: pointer;
border: none;
background: none;
}
table {
border-collapse: collapse;
}
_typography.scss¶
// src/base/_typography.scss
@use '../abstracts' as *;
body {
font-family: $font-sans;
font-size: $font-size-base;
line-height: $line-height-normal;
color: $color-text;
background-color: var(--color-bg, $color-bg);
}
h1, h2, h3, h4, h5, h6 {
font-weight: $font-weight-bold;
line-height: $line-height-tight;
color: var(--color-text, $color-text);
}
h1 { font-size: $font-size-4xl; }
h2 { font-size: $font-size-3xl; }
h3 { font-size: $font-size-2xl; }
h4 { font-size: $font-size-xl; }
h5 { font-size: $font-size-lg; }
h6 { font-size: $font-size-base; }
p {
line-height: $line-height-relaxed;
margin-bottom: $spacing-md;
}
a {
color: var(--color-primary, $color-primary);
@include transition(color);
&:hover {
color: var(--color-primary-dark, $color-primary-dark);
}
&:focus-visible {
outline: 2px solid var(--color-primary, $color-primary);
outline-offset: 2px;
}
}
strong { font-weight: $font-weight-bold; }
small { font-size: $font-size-sm; }
code {
font-family: $font-mono;
font-size: 0.9em;
background-color: var(--color-bg-secondary, $color-bg-secondary);
padding: 2px 6px;
border-radius: $border-radius-sm;
}
hr {
border: none;
border-top: $border-width solid var(--color-border, $color-border);
margin: $spacing-lg 0;
}
::selection {
background-color: var(--color-primary, $color-primary);
color: $color-text-inverse;
}
_global.scss¶
// src/base/_global.scss
@use '../abstracts' as *;
.sr-only {
@include visually-hidden;
}
.container {
width: 100%;
max-width: $container-max-width;
margin-left: auto;
margin-right: auto;
padding-left: $container-padding;
padding-right: $container-padding;
}
// Respect de prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
3. Layout¶
_index.scss¶
// src/layout/_index.scss
@use '../abstracts' as *;
@forward 'grid';
@forward 'container';
@forward 'header';
@forward 'footer';
_grid.scss¶
// src/layout/_grid.scss
@use '../abstracts' as *;
.row {
display: flex;
flex-wrap: wrap;
margin-left: -$spacing-2;
margin-right: -$spacing-2;
}
.col {
flex: 1;
padding-left: $spacing-2;
padding-right: $spacing-2;
}
// Génération des colonnes
@for $i from 1 through 12 {
.col-#{$i} {
flex: 0 0 calc(#{$i} / 12 * 100%);
max-width: calc(#{$i} / 12 * 100%);
padding-left: $spacing-2;
padding-right: $spacing-2;
}
}
// Responsive : md
@include respond-to('md') {
@for $i from 1 through 12 {
.col-md-#{$i} {
flex: 0 0 calc(#{$i} / 12 * 100%);
max-width: calc(#{$i} / 12 * 100%);
}
}
}
// Responsive : lg
@include respond-to('lg') {
@for $i from 1 through 12 {
.col-lg-#{$i} {
flex: 0 0 calc(#{$i} / 12 * 100%);
max-width: calc(#{$i} / 12 * 100%);
}
}
}
// Grid CSS moderne
.grid {
display: grid;
gap: $spacing-md;
&--2 { grid-template-columns: repeat(2, 1fr); }
&--3 { grid-template-columns: repeat(3, 1fr); }
&--4 { grid-template-columns: repeat(4, 1fr); }
&--auto {
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
}
@include respond-to('md') {
.grid {
&--md-2 { grid-template-columns: repeat(2, 1fr); }
&--md-3 { grid-template-columns: repeat(3, 1fr); }
&--md-4 { grid-template-columns: repeat(4, 1fr); }
}
}
_container.scss¶
// src/layout/_container.scss
@use '../abstracts' as *;
.container {
width: 100%;
max-width: $container-max-width;
margin-left: auto;
margin-right: auto;
padding-left: $container-padding;
padding-right: $container-padding;
&--narrow {
max-width: 800px;
}
&--wide {
max-width: 1400px;
}
&--full {
max-width: none;
padding-left: 0;
padding-right: 0;
}
}
_header.scss¶
// src/layout/_header.scss
@use '../abstracts' as *;
.header {
position: sticky;
top: 0;
z-index: $z-sticky;
background-color: var(--color-bg, $color-bg);
box-shadow: $shadow-sm;
border-bottom: $border-width solid var(--color-border, $color-border);
@include transition(background-color, box-shadow);
&__inner {
@include flex-between;
max-width: $container-max-width;
margin: 0 auto;
padding: $spacing-3 $container-padding;
}
&__brand {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
color: var(--color-primary, $color-primary);
letter-spacing: -0.025em;
}
&__nav {
@include flex-center;
gap: $spacing-1;
}
&__link {
padding: $spacing-1 $spacing-3;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: var(--color-text-secondary, $color-text-secondary);
border-radius: $border-radius-md;
@include transition(color, background-color);
&:hover {
color: var(--color-text, $color-text);
background-color: var(--color-bg-secondary, $color-bg-secondary);
}
&--active {
color: var(--color-primary, $color-primary);
background-color: var(--color-bg-secondary, $color-bg-secondary);
}
}
&__toggle {
display: block;
padding: $spacing-2;
@include respond-to('md') {
display: none;
}
}
@include respond-below('md') {
&__nav {
display: none;
&--open {
display: flex;
flex-direction: column;
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--color-bg, $color-bg);
box-shadow: $shadow-md;
padding: $spacing-4;
gap: $spacing-1;
}
}
}
}
_footer.scss¶
// src/layout/_footer.scss
@use '../abstracts' as *;
.footer {
background-color: var(--color-text, $color-text);
color: var(--color-text-inverse, $color-text-inverse);
padding: $spacing-16 0 $spacing-8;
&__inner {
max-width: $container-max-width;
margin: 0 auto;
padding: 0 $container-padding;
display: grid;
gap: $spacing-8;
grid-template-columns: 1fr;
@include respond-to('md') {
grid-template-columns: 2fr 1fr 1fr;
}
}
&__brand {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
margin-bottom: $spacing-4;
}
&__text {
opacity: 0.7;
line-height: $line-height-relaxed;
}
&__title {
font-size: $font-size-sm;
font-weight: $font-weight-bold;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: $spacing-4;
opacity: 0.5;
}
&__link {
display: block;
color: inherit;
opacity: 0.7;
padding: $spacing-1 0;
@include transition(opacity);
&:hover {
opacity: 1;
}
}
&__bottom {
max-width: $container-max-width;
margin: $spacing-8 auto 0;
padding: $spacing-4 $container-padding 0;
border-top: $border-width solid rgba(255, 255, 255, 0.1);
text-align: center;
font-size: $font-size-sm;
opacity: 0.5;
}
}
4. Components¶
_index.scss¶
// src/components/_index.scss
@use '../abstracts' as *;
@forward 'buttons';
@forward 'cards';
@forward 'forms';
@forward 'badges';
@forward 'alerts';
_buttons.scss¶
// src/components/_buttons.scss
@use '../abstracts' as *;
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: $spacing-2;
padding: $spacing-2 $spacing-5;
font-family: $font-sans;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
line-height: 1;
border-radius: $border-radius-md;
border: 2px solid transparent;
cursor: pointer;
@include transition(all);
&:focus-visible {
outline: 2px solid var(--color-primary, $color-primary);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
// --- Variantes ---
&--primary {
background-color: var(--color-primary, $color-primary);
color: $color-text-inverse;
&:hover {
background-color: var(--color-primary-dark, $color-primary-dark);
}
}
&--secondary {
background-color: var(--color-secondary, $color-secondary);
color: $color-text-inverse;
&:hover {
background-color: var(--color-secondary-dark, $color-secondary-dark);
}
}
&--danger {
background-color: var(--color-danger, $color-danger);
color: $color-text-inverse;
&:hover {
background-color: var(--color-danger-dark, $color-danger-dark);
}
}
&--outline {
background-color: transparent;
border-color: var(--color-primary, $color-primary);
color: var(--color-primary, $color-primary);
&:hover {
background-color: var(--color-primary, $color-primary);
color: $color-text-inverse;
}
}
&--ghost {
background-color: transparent;
color: var(--color-primary, $color-primary);
&:hover {
background-color: var(--color-bg-secondary, $color-bg-secondary);
}
}
// --- Tailles ---
&--sm {
padding: $spacing-1 $spacing-3;
font-size: $font-size-xs;
}
&--lg {
padding: $spacing-3 $spacing-6;
font-size: $font-size-base;
}
&--block {
display: flex;
width: 100%;
}
// --- États ---
&--loading {
position: relative;
color: transparent !important;
pointer-events: none;
&::after {
content: '';
position: absolute;
@include size(16px);
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
}
}
@keyframes spin {
to { transform: rotate(360deg); }
}
_cards.scss¶
// src/components/_cards.scss
@use '../abstracts' as *;
.card {
background-color: var(--color-bg, $color-bg);
border: $border-width solid var(--color-border, $color-border);
border-radius: $border-radius-lg;
overflow: hidden;
@include transition(box-shadow, transform);
&:hover {
box-shadow: $shadow-lg;
}
&--clickable {
cursor: pointer;
&:hover {
transform: translateY(-2px);
}
&:focus-visible {
outline: 2px solid var(--color-primary, $color-primary);
outline-offset: 2px;
}
}
&--flat {
box-shadow: none;
border: $border-width solid var(--color-border, $color-border);
&:hover {
box-shadow: none;
border-color: var(--color-primary, $color-primary);
}
}
&__image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
&__body {
padding: $spacing-5;
}
&__header {
padding: $spacing-5;
border-bottom: $border-width solid var(--color-border, $color-border);
}
&__footer {
padding: $spacing-4 $spacing-5;
border-top: $border-width solid var(--color-border, $color-border);
background-color: var(--color-bg-secondary, $color-bg-secondary);
display: flex;
align-items: center;
justify-content: flex-end;
gap: $spacing-2;
}
&__title {
font-size: $font-size-xl;
font-weight: $font-weight-bold;
margin-bottom: $spacing-2;
color: var(--color-text, $color-text);
}
&__subtitle {
font-size: $font-size-sm;
color: var(--color-text-secondary, $color-text-secondary);
margin-bottom: $spacing-3;
}
&__text {
color: var(--color-text-secondary, $color-text-secondary);
line-height: $line-height-relaxed;
@include line-clamp(3);
}
&__meta {
display: flex;
align-items: center;
gap: $spacing-2;
font-size: $font-size-xs;
color: var(--color-text-muted, $color-text-muted);
margin-top: $spacing-3;
}
// --- Layout horizontal ---
&--horizontal {
@include respond-to('md') {
display: flex;
.card__image {
width: 240px;
flex-shrink: 0;
aspect-ratio: auto;
height: 100%;
}
}
}
}
_forms.scss¶
// src/components/_forms.scss
@use '../abstracts' as *;
.form {
&__group {
margin-bottom: $spacing-5;
}
&__label {
display: block;
font-size: $font-size-sm;
font-weight: $font-weight-medium;
color: var(--color-text, $color-text);
margin-bottom: $spacing-1-5;
}
&__required {
color: var(--color-danger, $color-danger);
margin-left: 2px;
}
&__input,
&__textarea,
&__select {
display: block;
width: 100%;
padding: $spacing-2-5 $spacing-3;
font-family: $font-sans;
font-size: $font-size-sm;
color: var(--color-text, $color-text);
background-color: var(--color-bg, $color-bg);
border: 2px solid var(--color-border, $color-border);
border-radius: $border-radius-md;
@include transition(border-color, box-shadow);
&:hover {
border-color: var(--color-border-hover, $color-border-hover);
}
&:focus {
outline: none;
border-color: var(--color-primary, $color-primary);
box-shadow: 0 0 0 3px rgba($color-primary, 0.15);
}
&::placeholder {
color: var(--color-text-muted, $color-text-muted);
}
&--error {
border-color: var(--color-danger, $color-danger);
&:focus {
box-shadow: 0 0 0 3px rgba($color-danger, 0.15);
}
}
&--success {
border-color: var(--color-success, $color-success);
&:focus {
box-shadow: 0 0 0 3px rgba($color-success, 0.15);
}
}
}
&__textarea {
min-height: 120px;
resize: vertical;
}
&__select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right $spacing-3 center;
padding-right: $spacing-10;
}
&__checkbox,
&__radio {
display: flex;
align-items: center;
gap: $spacing-2;
cursor: pointer;
font-size: $font-size-sm;
input {
width: 18px;
height: 18px;
accent-color: var(--color-primary, $color-primary);
cursor: pointer;
}
}
&__help {
font-size: $font-size-xs;
color: var(--color-text-muted, $color-text-muted);
margin-top: $spacing-1;
}
&__error {
font-size: $font-size-xs;
color: var(--color-danger, $color-danger);
margin-top: $spacing-1;
display: flex;
align-items: center;
gap: 4px;
}
&__row {
display: grid;
gap: $spacing-4;
@include respond-to('md') {
grid-template-columns: repeat(2, 1fr);
}
}
&__actions {
display: flex;
gap: $spacing-3;
margin-top: $spacing-6;
@include respond-to('md') {
justify-content: flex-end;
}
}
}
_badges.scss¶
// src/components/_badges.scss
@use '../abstracts' as *;
.badge {
display: inline-flex;
align-items: center;
padding: $spacing-0-5 $spacing-2;
font-size: 11px;
font-weight: $font-weight-bold;
line-height: 1.5;
border-radius: $border-radius-full;
text-transform: uppercase;
letter-spacing: 0.05em;
&--primary {
background-color: rgba($color-primary, 0.15);
color: var(--color-primary, $color-primary);
}
&--secondary {
background-color: rgba($color-secondary, 0.15);
color: var(--color-secondary-dark, $color-secondary-dark);
}
&--danger {
background-color: rgba($color-danger, 0.15);
color: var(--color-danger, $color-danger);
}
&--warning {
background-color: rgba($color-warning, 0.15);
color: var(--color-warning-dark, $color-warning-dark);
}
&--success {
background-color: rgba($color-success, 0.15);
color: var(--color-success-dark, $color-success-dark);
}
&--dark {
background-color: rgba($color-text, 0.15);
color: var(--color-text, $color-text);
}
&--pill {
padding: $spacing-0-5 $spacing-3;
}
}
_alerts.scss¶
// src/components/_alerts.scss
@use '../abstracts' as *;
.alert {
padding: $spacing-4 $spacing-5;
border-radius: $border-radius-md;
border-left: 4px solid;
margin-bottom: $spacing-4;
&--success {
background-color: rgba($color-success, 0.1);
border-color: $color-success;
color: $color-success-dark;
}
&--danger {
background-color: rgba($color-danger, 0.1);
border-color: $color-danger;
color: $color-danger-dark;
}
&--warning {
background-color: rgba($color-warning, 0.1);
border-color: $color-warning;
color: $color-warning-dark;
}
&--info {
background-color: rgba($color-primary, 0.1);
border-color: $color-primary;
color: $color-primary-dark;
}
&__title {
font-weight: $font-weight-bold;
margin-bottom: $spacing-1;
}
&__text {
line-height: $line-height-relaxed;
}
&__close {
float: right;
font-size: $font-size-lg;
line-height: 1;
opacity: 0.5;
@include transition(opacity);
&:hover {
opacity: 1;
}
}
}
5. Themes¶
_index.scss¶
_light.scss¶
// src/themes/_light.scss
:root,
[data-theme="light"] {
--color-primary: #{$color-primary};
--color-primary-light: #{$color-primary-light};
--color-primary-dark: #{$color-primary-dark};
--color-secondary: #{$color-secondary};
--color-secondary-dark: #{$color-secondary-dark};
--color-danger: #{$color-danger};
--color-danger-dark: #{$color-danger-dark};
--color-warning: #{$color-warning};
--color-warning-dark: #{$color-warning-dark};
--color-success: #{$color-success};
--color-success-dark: #{$color-success-dark};
--color-text: #{$color-text};
--color-text-secondary: #{$color-text-secondary};
--color-text-muted: #{$color-text-muted};
--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};
}
_dark.scss¶
// src/themes/_dark.scss
[data-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-secondary-dark: #{$color-secondary};
--color-danger: #{lighten($color-danger, 10%)};
--color-danger-dark: #{$color-danger};
--color-warning: #{lighten($color-warning, 10%)};
--color-warning-dark: #{$color-warning};
--color-success: #{lighten($color-success, 10%)};
--color-success-dark: #{$color-success};
--color-text: #e2e8f0;
--color-text-secondary: #a0aec0;
--color-text-muted: #718096;
--color-bg: #1a202c;
--color-bg-secondary: #2d3748;
--color-bg-tertiary: #4a5568;
--color-border: #4a5568;
--color-border-hover: #718096;
}
6. Utilities¶
_index.scss¶
// src/utilities/_index.scss
@use '../abstracts' as *;
@forward 'display';
@forward 'text';
@forward 'spacing';
@forward 'generated';
_display.scss¶
// src/utilities/_display.scss
.u-hidden { display: none !important; }
.u-block { display: block !important; }
.u-flex { display: flex !important; }
.u-grid { display: grid !important; }
.u-inline { display: inline !important; }
.u-inline-block { display: inline-block !important; }
.u-inline-flex { display: inline-flex !important; }
.u-flex-center { @include flex-center; }
.u-flex-between { @include flex-between; }
.u-flex-col { @include flex-column; }
.u-flex-wrap { flex-wrap: wrap !important; }
.u-items-center { align-items: center !important; }
.u-items-start { align-items: flex-start !important; }
.u-items-end { align-items: flex-end !important; }
.u-justify-center { justify-content: center !important; }
.u-justify-between { justify-content: space-between !important; }
.u-justify-end { justify-content: flex-end !important; }
.u-w-full { width: 100% !important; }
.u-w-auto { width: auto !important; }
.u-max-w-sm { max-width: 540px !important; }
.u-max-w-md { max-width: 720px !important; }
.u-max-w-lg { max-width: 960px !important; }
.u-max-w-xl { max-width: 1140px !important; }
.u-mx-auto { margin-left: auto !important; margin-right: auto !important; }
.u-overflow-hidden { overflow: hidden !important; }
.u-relative { position: relative !important; }
.u-absolute { position: absolute !important; }
.u-sticky { position: sticky !important; }
_text.scss¶
// src/utilities/_text.scss
.u-text-center { text-align: center !important; }
.u-text-left { text-align: left !important; }
.u-text-right { text-align: right !important; }
.u-text-uppercase { text-transform: uppercase !important; }
.u-text-lowercase { text-transform: lowercase !important; }
.u-text-capitalize { text-transform: capitalize !important; }
.u-text-truncate { @include truncate; }
.u-text-primary { color: var(--color-primary, $color-primary) !important; }
.u-text-danger { color: var(--color-danger, $color-danger) !important; }
.u-text-success { color: var(--color-success, $color-success) !important; }
.u-text-muted { color: var(--color-text-muted, $color-text-muted) !important; }
.u-text-white { color: $color-text-inverse !important; }
.u-font-light { font-weight: $font-weight-light !important; }
.u-font-normal { font-weight: $font-weight-normal !important; }
.u-font-medium { font-weight: $font-weight-medium !important; }
.u-font-semibold { font-weight: $font-weight-semibold !important; }
.u-font-bold { font-weight: $font-weight-bold !important; }
@each $name, $value in $font-size-map {
.u-text-#{$name} {
font-size: $value !important;
}
}
_spacing.scss¶
// src/utilities/_spacing.scss
@each $size-name, $size-value in $spacing-map {
// Marges
.u-m-#{$size-name} { margin: $size-value !important; }
.u-mt-#{$size-name} { margin-top: $size-value !important; }
.u-mr-#{$size-name} { margin-right: $size-value !important; }
.u-mb-#{$size-name} { margin-bottom: $size-value !important; }
.u-ml-#{$size-name} { margin-left: $size-value !important; }
.u-mx-#{$size-name} {
margin-left: $size-value !important;
margin-right: $size-value !important;
}
.u-my-#{$size-name} {
margin-top: $size-value !important;
margin-bottom: $size-value !important;
}
// Paddings
.u-p-#{$size-name} { padding: $size-value !important; }
.u-pt-#{$size-name} { padding-top: $size-value !important; }
.u-pr-#{$size-name} { padding-right: $size-value !important; }
.u-pb-#{$size-name} { padding-bottom: $size-value !important; }
.u-pl-#{$size-name} { padding-left: $size-value !important; }
.u-px-#{$size-name} {
padding-left: $size-value !important;
padding-right: $size-value !important;
}
.u-py-#{$size-name} {
padding-top: $size-value !important;
padding-bottom: $size-value !important;
}
}
_generated.scss¶
// src/utilities/_generated.scss
// Auto-générer les classes de shadow
@each $name, $value in $shadow-map {
.u-shadow-#{$name} {
box-shadow: $value !important;
}
}
// Auto-générer les classes de border-radius
@each $name, $value in $border-radius-map {
.u-rounded-#{$name} {
border-radius: $value !important;
}
}
// Gaps
.u-gap-1 { gap: $spacing-1 !important; }
.u-gap-2 { gap: $spacing-2 !important; }
.u-gap-3 { gap: $spacing-3 !important; }
.u-gap-4 { gap: $spacing-4 !important; }
.u-gap-5 { gap: $spacing-5 !important; }
.u-gap-6 { gap: $spacing-6 !important; }
.u-gap-8 { gap: $spacing-8 !important; }
// Opacity
.u-opacity-0 { opacity: 0 !important; }
.u-opacity-25 { opacity: 0.25 !important; }
.u-opacity-50 { opacity: 0.5 !important; }
.u-opacity-75 { opacity: 0.75 !important; }
.u-opacity-100 { opacity: 1 !important; }
// Cursor
.u-cursor-pointer { cursor: pointer !important; }
.u-cursor-not-allowed { cursor: not-allowed !important; }
Exemple d'utilisation (index.html)¶
<!DOCTYPE html>
<html lang="fr" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mini Design System</title>
<link rel="stylesheet" href="dist/css/main.css">
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header__inner">
<a href="#" class="header__brand">MiniDS</a>
<nav class="header__nav">
<a href="#" class="header__link header__link--active">Accueil</a>
<a href="#" class="header__link">Composants</a>
<a href="#" class="header__link">Documentation</a>
</nav>
</div>
</header>
<!-- Hero -->
<section class="u-py-12 u-text-center">
<div class="container">
<h1 class="u-mb-4">Mini Design System</h1>
<p class="u-text-lg u-text-muted u-mb-6" style="max-width: 600px; margin-left: auto; margin-right: auto;">
Un système de design complet construit avec SCSS.
</p>
<div class="u-flex-center u-gap-3">
<button class="btn btn--primary btn--lg">Commencer</button>
<button class="btn btn--outline btn--lg">Documentation</button>
</div>
</div>
</section>
<!-- Cards -->
<section class="u-py-8" style="background: var(--color-bg-secondary);">
<div class="container">
<h2 class="u-text-center u-mb-8">Composants</h2>
<div class="grid grid--auto">
<div class="card card--clickable">
<div class="card__body">
<span class="badge badge--primary u-mb-3">Nouveau</span>
<h3 class="card__title">Boutons</h3>
<p class="card__text">Plusieurs variantes et tailles disponibles.</p>
</div>
</div>
<div class="card card--clickable">
<div class="card__body">
<span class="badge badge--success u-mb-3">Stable</span>
<h3 class="card__title">Cartes</h3>
<p class="card__text">Composant flexible pour afficher du contenu.</p>
</div>
</div>
<div class="card card--clickable">
<div class="card__body">
<span class="badge badge--warning u-mb-3">Beta</span>
<h3 class="card__title">Formulaires</h3>
<p class="card__text">Inputs, selects, checkboxes et plus.</p>
</div>
</div>
</div>
</div>
</section>
<!-- Theme Toggle -->
<script>
document.querySelector('.header__brand').addEventListener('click', () => {
const html = document.documentElement;
const current = html.getAttribute('data-theme');
html.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
});
</script>
</body>
</html>
Installation et build¶
# Initialiser le projet
mkdir mini-design-system && cd mini-design-system
npm init -y
npm install --save-dev sass
# Compiler
npx sass src/main.scss dist/css/main.css --style=compressed --source-map
# Watch
npx sass --watch src/main.scss:dist/css/main.css --source-map
// package.json (scripts)
{
"scripts": {
"build": "sass src/main.scss dist/css/main.css --style=compressed --source-map",
"dev": "sass --watch src/main.scss:dist/css/main.css --source-map",
"lint": "stylelint 'src/**/*.scss'"
}
}
Résumé¶
Ce Mini Design System démontre comment les concepts SCSS s'assemblent pour créer un système cohérent et maintenable. L'architecture 7-1, les @use/@forward, les tokens, les mixins et les utilities travaillent ensemble pour produire un CSS performant et un code source organisé. Ce projet peut servir de base à n'importe quel projet web réel.