Ce document rassemble les bonnes pratiques appliquées par l'agence web Alsacreations.fr concernant : "CSS". Ces guidelines CSS sont le fruit de plusieurs années d'expérience en méthodologies (OOCSS, BEM, CubeCSS) et frameworks (Bootstrap, Tailwind, UnoCSS) et sont destinées à constamment évoluer dans le temps et à s'adapter à chaque nouveau projet.
À ce jour, deux méthodes d'intégration CSS ont démontré leurs avantages en production : CSS "vanilla" (natif) et CSS utilitaire (via Tailwind ou UnoCSS par exemple).
Sauf contre-indication (client, projet historique) nous intégrons nos projets en CSS Vanilla.
Sommaire
- Guidelines : CSS
- Sommaire
- Configuration dans un projet
- Bonnes pratiques CSS globales
- Syntaxe
- Variables CSS (primitives et tokens)
- Unités
- Notation imbriquée (nesting)
- Breakpoints et Media Queries
- Transitions et animations
- Méthodes de positionnement
- Pseudo-classes et pseudo-éléments
- Dark Mode
- Polices (fonts)
- Media print (impression)
Configuration dans un projet
Tailwind
Nous intégrons nos styles en "CSS Vanilla", c'est à dire que ne faisons pas usage de classes utilitaires dans le HTML sauf rares exceptions (par exemple pour distinguer un élément parmi d'autres semblables).
Pour ce faire, un générateur de classes utilitaires Tailwind est incorporé dans nos projets afin de bénéficier de classes utilitaires lorsque cela est nécessaire.
L'installation et la configuration de Tailwind est décrite (dans le fichier project-init.md
)
PostCSS / Sass
Certaines fonctionnalités CSS indispensables ne sont actuellement pas réalisables en natif (Concaténation des fichiers, Mixins, Custom Media).
Selon les projets, deux options sont envisagées pour bénéficier de ces fonctionnalités :
- Le post-processeur PostCSS
- Le pré-processeur Sass (syntaxe
.scss
) dans nos projets d'intégration (non conseillé)
Quelle que soit la solution choisie, la méthode de compilation vers CSS dépend du type de projet (statique, Vue, Vite, Webpack, etc.).
Linters
Stylelint et Prettier sont utilisés pour vérifier la syntaxe et les bonnes pratiques CSS.
La configuration de ces linters est détaillée dans le guide project-init.md
.
Bonnes pratiques CSS globales
- Nous privilégions systématiquement l'usage de sélecteurs de class plutôt que les sélecteurs d'éléments (
li
,span
,p
) et ne ciblons jamais via un sélecteur#id
. - Nous évitons tant que possible les sélecteurs composés tels que
.modal span
ou.modal .date
mais plutôt.modal-date
pour conserver une spécificité minimale. - Nous employons les variables CSS plutôt que des valeurs "en dur" (ex. :
gap: var(--spacing-m)
plutôt quegap: 1rem
) et faisons référence aux tokens plutôt qu'au primitives si c'est possible (ex. :gap: var(--spacing-m)
plutôt quegap: var(--spacing-16)
)
Syntaxe
Ordre des déclarations
Les déclarations au sein d'une règle CSS sont ordonnées de façon à faire apparaître les propriétés importantes en tête de liste.
Les déclarations sont automatiquement réordonnées à l'aide de stylelint-order
en suivant l'ordre "smacss"
(voir la configuration dans le guide project-init.md
).
Variables CSS (primitives et tokens)
Les variables CSS (custom properties) du projet s'articulent en trois étapes :
- Les valeur primitives (ex.
--color-pink-300: #f9a8d4;
) - Les tokens, ou roles (ex.
--color-primary: var(--color-pink-300);
) (voir section suivante) - L'usage des tokens dans les styles des composants (ex.
color: var(--color-primary);
)
Primitives
Les valeurs primitives sont des valeurs de base issues de l'UI-Kit qui ne changent pas et qui sont utilisées pour définir les rôles (tokens) du projet. Un développeur n'est pas censé inventer de nouvelles primitives ni modifier ces valeurs. Si une valeur n'existe pas, il est nécessaire de la créer en concertation avec le designer.
/* fichier `app.css` */
/* valeurs d'exemple (toutes issues de l'UI-Kit) */
:root {
--color-pink-100: #fce7f3;
--color-pink-500: #f1498f;
--spacing-2: 0.125rem;
--spacing-8: 0.5rem;
--spacing-16: 1rem;
--font-base: system-ui, sans-serif;
--font-poppins: poppins, sans-serif;
--font-weight-regular: 400;
--font-weight-bold: 700;
--text-16: 1rem;
--text-18: 1.125rem;
--text-20: 1.25rem;
--radius-none: 0;
--radius-full: 9999px;
}
Règles de nommage des primitives
Pour assurer un workflow fluide entre designer et développeur, les variables sont nommées de manière codifiée par les deux parties.
Les règles de nommage sont les suivantes (issues de la documentation Tailwind 4) :
- Une couleur est préfixée par
--color-*
(ex.--color-pink-300
) - Un espacement (marge, padding, gouttière) est préfixé par
--spacing-*
(ex.--spacing-16
) - Une taille de police est préfixée par
--text-*
(ex.--text-16
) - Une famille de police est préfixée par
--font-*
(ex.--font-base
) - Une graisse de police est préfixée par
--font-weight-*
(ex.--font-weight-regular
) - Une
line-height
est préfixée par--leading-*
(ex.--leading-28
) - Un arrondi est préfixé par
--radius-*
(ex.--radius-lg
) - Une ombre est préfixée par
--shadow-*
(ex.--shadow-md
) - Un z-index est préfixé par
--z-*
(ex.--z-above-header-level
)
Tokens (=roles)
Les tokens sont des propriétés auxquelles des roles/fonctions ont été attibués.
- Un token fait référence à une valeur primitive : par exemple
--color-primary
fait référence à--color-pink-300
. - Un token est sémantique : le but est de savoir à quoi sert
--color-primary
ou--spacing-m
sans forcément connaître leurs style. - Un token est agnostique (décontextualisé) :
--color-primary
est OK en light ou dark mode,--spacing-m
est OK en desktop ou mobile.
/* fichier `styles.css` */
/* valeurs d'exemple à adapter au projet, évidemment */
:root {
--primary: var(--color-blue-500);
--surface: light-dark(var(--color-white), var(--color-gray-900));
--on-primary: var(--color-white);
--on-surface: light-dark(var(--color-gray-900), var(--color-white));
--spacing-m: clamp(var(--spacing-32), 1.5909rem + 1.8182vw, var(--spacing-48));
--spacing-l: clamp(var(--spacing-40), 1.4773rem + 4.5455vw, var(--spacing-80));
--text-m: clamp(var(--text-16), 0.9565rem + 0.2174vw, var(--text-18));
--text-2xl: clamp(var(--text-24), 1.3466rem + 0.6818vw, var(--text-30));
--link: light-dark(var(--color-blue-700), var(--color-blue-300));
--link-hover: light-dark(var(--color-blue-900), var(--color-blue-500));
--shadow: light-dark(#00000014, #ffffff14);
}
Liste des rôles des tokens
Cette liste est non exhaustive. Elle concerne les tokens les plus courants et dont la portée concerne l'ensemble du projet.
Nos tokens de couleurs (surface
, on-surface
, etc.) sont inspirés de Material Design.
Primary
: couleur d'accent principale (boutons, états actifs,…)Secondary
: couleur d'accent secondaireSurface
: aplat de couleur principal (généralement celle debody
)Surface Dim
: aplat de couleur secondaire (ici "obscurci")On Primary
: couleur de d'un élément posé surPrimary
(peut être du texte, une icône, etc.)On Surface
: couleur d'un élément posé surSurface
Layer
aplat de couleur d'un bloc posé sur une surfaceLayer High
aplat de couleur d'un bloc posé sur unLayer
On Layer
: couleur d'un élément posé surLayer
Link
: couleur des liensLink Hover
: couleur des liens au survol / focusOutline
: couleur des bordures (ex. inputs, textarea)Outline Hover
: couleur des bordures au survol / focusError
: couleur des messages d'erreurSuccess
: couleur des messages de succèsShadow
: couleur de l'ombre portéeSelection
: couleur de fond lors de la sélection de texteText M
: taille de police "moyenne" (peut être variable)Text L
: taille de police pour titres moyensText XL
: taille de police pour grosd titres
En plus de cette liste commune à tous projets, il est conseillé d'appliquer des tokens spécifiques à chacun des composants. Par exemple un composant "Tabs" (onglets) pourrait bénéficier de tokens tels que :
Tab Surface
: aplat de couleur d'un ongletsTab On Surface
: couleur du contenu des ongletsTab Outline
: couleur de la bordure des ongletsTab Surface Active
: aplat de couleur de l'onglet actifTab Layer
: aplat de couleur du contenu des onglets- etc.
Unités
- La première règle à observer est : "si la valeur doit pouvoir s'adapter à la taille de police de l'utilisateur, utiliser des
rem
, sinon utiliser despx
". Consulter l'article de Josh Comeau pour les détails et cas concrets. - La seconde règle est : "Éviter d'indiquer une taille à un élément, privilégier la fluidité (
1fr
dans Grid Layout,flex-grow
dans Flexbox) lorsque cela est possible". - La troisième règle est : "Éviter d'imposer une hauteur à un élément possédant du contenu tant que cela est possible".
On privilégie le rem
pour :
- La taille de police (
1rem
est équivalent à16px
) - Les Media Queries (
576px
=36rem
,992px
=62rem
,1400px
=87.5rem
)
On privilégie le px
pour :
- Les espacements verticaux et horizontaux entre les élements (gouttières, rythme vertical)
- Les dimensions d'éléments non dépendants de la taille de contenu (images)
Autres unités :
dvh
pour la hauteur (minimum) de page (body
)pt
exclusivement en feuille de styles print
Notation imbriquée (nesting)
Nous utilisons la notation imbriquée (nesting) de CSS natif car elle facilite la lecture et la maintenabilité du code en évitant de répéter les occurences de chaque sélecteur.
Le nesting est particulièrement préconisé pour :
- Les événements tels que
&:hover
,&:focus
,&:active
. - Les pseudo-classes telles que
&:first-child
,&:empty
, etc. - Les pseudo-éléments tels que
&::before
,&::after
. - Les media queries
@media ()
.
À privilégier (le nesting permet de réduire les duplications de sélecteurs) :
.wrapper {
&:hover, &:focus {}
&::before, &::after {}
@media (width >= 40rem) {
&::before {}
}
}
À éviter (le nesting peut conduire à augmenter la spécificité finale) :
.wrapper {
& .child {
& .subchild {
}
}
}
L'inconvénient de la notation imbriquée est qu'elle génère des sélecteurs CSS composés donc avec une spécificité qui augmente. Il est conseillé de limiter la syntaxe à un seul niveau d'imbrication.
Breakpoints et Media Queries
La liste de points de rupture (breakpoints) figure dans la configuration du contructeur de classes utilitaires.
Sauf contre-indication selon projet, on privilégie la méthode Mobile First et les valeurs des breakpoints sont exprimées en unité rem
:
40rem
// correspond à 640px48rem
// 768px64rem
// 1024px80rem
// 1280px96rem
// 1536px
Nous utilisons de préférence la syntaxe "moderne" des Media Queries :
/* composant card sur écran "640" ou plus */
.card {
display: flex;
@media (width >= 40rem) {
flex-direction: column;
}
}
Pour éviter les collisions d'intervalles de media queries, notre convention est :
- En mobile first (conseillé) on inclut la valeur, donc "=" ->
@media (width >= 48rem)
- En desktop first, on exclut la valeur, donc pas de "=" ->
@media (width < 48rem)
Transitions et animations
- Éviter d’animer des propriétés autres que
transform
(translate
,rotate
,scale
) ouopacity
oufilter
(ou alors ajouter la propriétéwill-change
au cas par cas). - Toujours préciser quelle(s) propriété(s) doit être animée dans une transition ou animation. Par exemple
transition: 0.5s scale
.
Animer du SVG
Quelques précautions sont à prendre concernant les SVG :
- Toujours compresser le fichier à l'aide de SVGOMG
- Donner des noms de classe à chaque
path
qui doit être animé - Appliquer les styles CSS suivants…
svg {
/* Par défaut les navigateurs masquent ce qui dépasse du Viewbox */
/* ressource : https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/overflow */
overflow: visible;
}
svg * {
/* Par défaut le référent pour transform-origin est l'ensemble du SVG (view-box) */
/* ressource : https://developer.mozilla.org/en-US/docs/Web/CSS/transform-box#svg_transform-origin_scoping */
transform-box: fill-box;
}
Méthodes de positionnement
Nous privilégions Grid Layout en priorité (aidé de grid area autant que possible), puis Flexbox en tenant compte de certains points d'attention.
Grid Layout
Grid Layout est choisi en priorité pour les avantages suivants :
- Affichage vertical par défaut
- Gère parfaitement les deux axes à la fois
- Place très précisément les éléments
- Permet de cibler uniquement le parent
- A peu de comportements contre-intuitifs
- Les areas offrent une représentation visuelle idéale
- Gère très bien le Responsive via Media Queries
Les inconvénients majeurs de Grid Layout sont :
- Gère moins bien le Responsive via taille du contenu ou Container Queries
Flexbox
Flexbox est choisi en priorité pour les avantages suivants :
- Affichage horizontal par défaut
- Passage à la ligne (wrap) d'enfants de tailles différentes
- Centrage simple de rangées multiples
- Grande liberté donnée aux enfants (grow, shrink)
- Prévoit un affichage inversé (*-reverse)
- Permet de se passer de Media / Container Queries
Les inconvénients majeurs de Flexbox sont :
- Ne gère pas bien les deux axes en même temps
- De nombreux comportements contre-intuitifs (alignements, shrink, min-width)
👉 Lorsqu'aucune des deux méthodes ne sort clairement du lot, alors Grid Layout est notre choix par défaut.
🔖 "When to use Flexbox and when to use CSS Grid"
Autres positionnements
position: absolute
: nécessaire pour placer un élément en "overlay" (par-dessus d'autres éléments). Le référent est le premier ancêtre lui-même positionné.position: relative
: utile principalement pour servir de référent à un descendant enabsolute
. Ne pas déplacer des éléments via cette position, privilégier systématiquement les transformations (translate: x y;
).position: static
: valeur par défaut deposition
.position: sticky
: permet de faire coller un élément aux bords de la fenêtre (ex. un header). Le référent est le Viewport. Nécessite un point d'ancrage (ex.top: 0
).float
: permet à un élément de se placer à gauche ou droite et que le contenu suivant s'écoule autour. Uniquement utile pour "habiller" une image.
Pseudo-classes et pseudo-éléments
Les pseudo-classes s'écrivent avec :
, les pseudo-éléments s'écrivent avec ::
.
Pseudo-éléments
- Les pseudo-élements les plus courants sont
::before
et::after
. - Ils nécessitent la propriété
content: "contenu"
pour être affichés. - Leur contenu est restitué (lu) par les assistances techniques, mais cela n'est toujours pas validé par RGAA donc il ne faut pas apporter des informations à l'aide de pseudo-éléments (ex. ne pas écrire ceci
content: "(lien externe)"
nicontent: "[↗]"
). - Nesting : Les pseudo-éléments sont de bons candidats à la syntaxe imbriquée telle que
&::before
,&::after
. - Les pseudo-éléments sont à rédiger en CSS vanilla et non en classe utilitaire (éviter
class="before:content-['Hello_World']"
)
Pseudo-classes
- Il existe une 60aine de pseudo-classes
- Nesting : Les pseudo-classes sont de bons candidats à la syntaxe imbriquée telle que
&:first-child
,&:empty
, etc. - Les pseudo-classes sont à rédiger en CSS natif et non en classe utilitaire.
Dark Mode
Le mode d'apparence (Light Mode, Dark Mode) est un paramètre dont l'utilisateur doit pouvoir bénéficier pour ses préférences personnelles ou pour des besoins spécifiques.
Il existe deux moyens pour un utilisateur de modifier le mode d'apparence des pages web :
- Via ses réglages système (ou via son navigateur)
- Via un bouton "theme switcher" intégré au site web
Dark Mode déclenché via les réglages système uniquement (non conseillé)
La Media Query (prefers-color-scheme: dark)
détecte les préférences système et permet de s'y adapter en CSS, mais la syntaxe de la fonction light-dark()
est plus intéressante et évite des imbrications inutiles.
À privilégier (light-dark()
) :
:root {
color-scheme: light dark;
--color-burger: light-dark(var(--color-red-700), var(--color-red-300));
}
.burger-text {
fill: var(--color-burger);
}
À éviter ((prefers-color-scheme: dark)
) :
:root {
color-scheme: light dark;
--color-burger: var(--color-red-700);
@media (prefers-color-scheme: dark) {
--color-burger: var(--color-red-300);
}
}
.burger-text {
fill: var(--color-burger);
}
Dark Mode déclenché via un bouton "theme switcher" (conseillé)
En plus de ses préférences par défaut, il est conseillé de proposer au visiteur de pouvoir décider de son mode d'apparence au cas par cas à l'aide d'un "theme switcher".
Voici un exemple de Switcher accessible sur Codepen : https://codepen.io/alsacreations/pen/ExBPExE
Le bouton de theme modifie l'attribut data-theme
sur html
, on s'en servira côté CSS pour forcer la valeur de color-scheme
:
:root {
color-scheme: light dark;
&[data-theme="light"] {
color-scheme: light;
}
&[data-theme="dark"] {
color-scheme: dark;
}
}
La fonction light-dark()
vue dans la partie précédente sera parfaitement adaptée là aussi pour gérer dynamiquement les couleurs quel que soit le mode adopté (préférences système ou choix manuel utilisateur).
Dark Mode et SVG inline :
- De manière générale utiliser
currentcolor
pour les couleurs desstroke
etfill
des SVG inline. Cela permet de s'adapter automatiquement à la valeur decolor
du parent. - Utiliser
light-dark()
pour pour appliquer des couleurs spécifiques au SVG sans être dépendant de la couleur du parent. Ex.fill: light-dark(var(--couleur-light), var(--couleur-dark));
Dark Mode et SVG externe (on peut toucher au SVG) :
Ajouter un élément <style>
dans le SVG pour appliquer les styles CSS suivants (ici la classe .path
a été ajoutée à l'élément dont la couleur doit s'adapter) :
<svg width="" height="" viewBox="" fill="none">
<style>
@media (prefers-color-scheme: dark) {
.path {
fill: white; /* valeur en dur ou currentcolor */
}
}
[data-theme="dark"] .path {
fill: white;
}
</style>
<path class="path" d="" fill="black" />
</svg>
Dark Mode et SVG externe (on ne peut pas toucher au SVG) :
Il est possible d'appliquer un masque CSS sur une image externe (le rendu final sera monochrome) :
<span class="icon"></span>
.icon {
--svg: url("images/burger.svg");
display: inline-block;
width: 200px;
height: 200px;
background-color: currentColor;
mask: var(--svg) no-repeat center;
mask-size: contain;
}
Dark Mode et ::selection
:
- Les custom properties CSS ne sont pas supportées dans
::selection
donc il faut définir les couleurs en dur. light-dark()
n'est pas supporté, il faut une media query ou un[data-theme=dark]
.- L'imbrication (nesting) n'est pas supportée (on ne peut pas
[data-theme=dark] & {}
).
Ce qui fonctionne :
::selection {
background-color: pink;
}
[data-theme="dark"] ::selection {
background-color: hotpink;
}
Polices (fonts)
Recommandations générales
- On privilégie la police système
system-ui
pour les textes de contenus (raison : performance + UX + Layout Shifts). - On privilégie le format
.woff2
. - On limite à 2 ou 3 fichiers de police au maximum (regular, bold, italic), sinon préférer une Variable Font (voir la partie dédiée ci-dessous)
- On utilise la directive
<link rel="preload">
pour charger les polices de manière asynchrone. - On applique
font-display: swap;
au sein de la règle@font-face
pour éviter les effets de FOIT. Si la police est pré-chargée,font-display: optional;
est alors recommandé. - On héberge la police sur son propre serveur (voir l'outil "Google Webfont Helper").
- On utilise les valeurs chiffrées pour les graisses de police (
font-weight
) :100
plutôt quethin
200
plutôt queextralight
300
plutôt quelight
400
plutôt quenormal
500
plutôt quemedium
600
plutôt quesemibold
700
plutôt quebold
800
plutôt queextrabold
900
plutôt queblack
🔖 https://www.debugbear.com/blog/website-font-performance
Outils d'optimisation et de tests de polices
- FontSquirrel webfont generator : https://www.fontsquirrel.com/tools/webfont-generator (ou Transfonter : https://transfonter.org/)
- Wakamai Fondue : https://wakamaifondue.com/
- Glyphhanger (via
npm
) : https://github.com/zachleat/glyphhanger
Code recommandé pour les polices
Voici un exemple de chargement de police conseillé (cas de deux fichiers de police regular et bold) :
<!-- Dans le <head> après
la feuille de styles pour ne pas la bloquer -->
<link rel="preload" as="font" href="kiwi.woff2" type="font/woff2" crossorigin="anonymous">
<link rel="preload" as="font" href="kiwi-bold.woff2" type="font/woff2" crossorigin="anonymous">
⚠️ Noter ci-dessous que le nom de la font-family est toujours le même ("kiwi") et qu'il ne faut pas confondre avec le nom du fichier.
@font-face {
font-family: "kiwi";
src: url("kiwi.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap; /* ou "optional" pour éviter les layout shifts */
}
@font-face {
font-family: "kiwi";
src: url("kiwi-bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
Google Webfont Helper
Google Webfont Helper génère le code CSS nécessaire, optimise finement les fichiers et permet de les héberger sans faire appel à Google en choisissant le bon subset (latin, latin-ext, etc.), les variantes (normal, bold, italic, etc.)
Cas des Variable Fonts
Une variable font est systématiquement recommandée dès lors qu'un projet nécessite plus de 3 ou 4 variantes parmi celles-ci : regular, italic, light, semi-bold, bold, bold italic, etc. Cette fonctionnalité est aujourd'hui reconnue par plus de 95% des navigateurs.
Comme pour les fontes classiques, le format .woff2
ainsi que l'hébergement de la fonte sont préconisés (les fontes variables peuvent être trouvées sur Google Fonts en activant la case "show only variable fonts" puis téléchargées en .ttf
via le bouton "Download family". Un convertisseur tel que Cloud converter pourra produire la version .woff2
.
Code recommandé pour les variable fonts :
@font-face {
font-family: "variable";
src:
url("variable.woff2") format("woff2") tech("variations"),
url("variable.woff2") format("woff2-variations");
font-display: swap;
font-weight: 100 900;
}
Modification des variantes (axis)
Toutes les variantes d'une fonte variable sont modifiables via la propriété font-variation-settings
. Certains de ces axis sont normalisés et disposent d'un équivalent en propriété CSS.
Ainsi, pour modifier la graisse d'une police, les deux syntaxes sont possibles : font-variation-settings: 'wght' 625;
ou font-weight: 625;
. Il est même possible de passer par une variable CSS ainsi font-variation-settings: 'wght' var(--text-weight);
Media print (impression)
Nous proposons une feuille de styles "Print" dans nos projets d'intégration web.
La feuille de styles dédiée à l'impression aide aussi à l'export PDF dans le navigateur. La plupart du temps il s'agira en priorité de masquer les éléments inutiles dans un document statique ou papier (ex : navigation) et de retirer les décorations superflues.