Le CSS évolue à grande vitesse. Découvrez les nouvelles fonctionnalités natives qui révolutionnent l'écriture de styles : CSS Nesting, Container Queries, @layer, :has(), @scope, Logica Properties, Scroll Snap, les nouvelles couleurs, @starting-style et le Popover API.
Le CSS a considérablement évolué ces dernières années. Les navigateurs ajoutent des fonctionnalités qui rendent le code plus court, plus maintenable et plus puissant. Voici ce que nous allons couvrir :
NOUVELLES FONCTIONNALITÉS CSS 2023-2026 ═══════════════════════════════════════════ 🔧 ORGANISATION 📐 LAYOUT & RÉPONSIVE ────────────── ───────────────────── • CSS Nesting • Container Queries • @layer • Scroll Snap • @scope • Logical Properties 🎨 COULEURS 🧩 SÉLECTEURS ──────────── ────────────── • oklch / oklab • :has() • color-mix() • @starting-style • accent-color 🪟 COMPOSANTS NATIFS 🌗 THÉMES ───────────────── ────────── • Popover API • color-scheme • @starting-style
Panorama des nouvelles fonctionnalités CSS
| Fonctionnalité | Chrome | Firefox | Safari | Date de support |
|---|---|---|---|---|
| CSS Nesting | 120+ ✓ | 117+ ✓ | 17.2+ ✓ | 2023 |
| @layer | 99+ ✓ | 97+ ✓ | 15.4+ ✓ | 2022 |
| Container Queries | 105+ ✓ | 110+ ✓ | 16+ ✓ | 2023 |
| :has() | 105+ ✓ | 121+ ✓ | 15.4+ ✓ | 2023 |
| @scope | 118+ ✓ | ⚠ Partiel | ✗ | 2024 |
| Logical Properties | 69+ ✓ | 55+ ✓ | 12.1+ ✓ | 2020 |
| Scroll Snap | 69+ ✓ | 62+ ✓ | 11+ ✓ | 2020 |
| accent-color | 93+ ✓ | 92+ ✓ | 15.4+ ✓ | 2021 |
| color-scheme | 81+ ✓ | 67+ ✓ | 12.1+ ✓ | 2020 |
| oklch / oklab | 111+ ✓ | 113+ ✓ | 15.4+ ✓ | 2023 |
| color-mix() | 111+ ✓ | 113+ ✓ | 16.2+ ✓ | 2023 |
| @starting-style | 117+ ✓ | ⚠ Partiel | 17.5+ ✓ | 2024 |
| Popover API | 114+ ✓ | 125+ ✓ | 17+ ✓ | 2024 |
/* AVANT — Sans nesting (CSS classique) */
.card { background: white; border-radius: 12px; }
.card__title { font-weight: 700; color: #1e293b; }
.card__text { color: #64748b; line-height: 1.6; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.card--featured { border-color: #2563eb; }
.card--featured .card__title { color: #2563eb; }
/* APRÈS — Avec CSS Nesting */
.card {
background: white;
border-radius: 12px;
/* Nesting d'éléments (comme BEM) */
&__title {
font-weight: 700;
color: #1e293b;
}
&__text {
color: #64748b;
line-height: 1.6;
}
/* Nesting de pseudo-classes */
&:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
/* Nesting de modificateurs */
&--featured {
border-color: #2563eb;
& .card__title {
color: #2563eb;
}
}
}
&/* & fait référence au sélecteur parent */
.btn {
padding: 0.5rem 1rem;
/* & = .btn */
&:hover { background: darker; }
&:focus { box-shadow: ring; }
&:active { transform: scale(0.98); }
/* & permet de remonter dans l'arbre */
.sidebar & {
padding: 0.25rem 0.5rem;
}
/* .sidebar .btn { ... } */
}
/* Nesting de media queries */
.card {
padding: 1rem;
@media (min-width: 768px) {
padding: 2rem;
}
}
/* Nesting de :has() */
.form-group {
margin-bottom: 1rem;
&:has(input:focus) {
background: #eff6ff;
}
}
/* Les at-rules s'imbriquent directement */
.component {
color: #333;
@media (min-width: 768px) {
color: #111;
font-size: 1.1rem;
}
@supports (backdrop-filter: blur(10px)) {
background: rgba(255,255,255,0.8);
backdrop-filter: blur(10px);
}
}
& devant les pseudo-classes depuis Chrome 120.
@layer permet de créer des couches de cascade explicites, donnant un contrôle total sur l'ordre de priorité des styles. C'est la réponse du CSS aux problèmes de spécificité et aux conflits entre bibliothèques.
/* Déclarer des couches dans l'ordre de priorité */
@layer base, components, utilities;
/* Couche 1 : Styles de base (priorité la plus basse) */
@layer base {
body { font-family: sans-serif; color: #333; }
h1 { font-size: 2rem; margin: 0; }
}
/* Couche 2 : Composants (priorité moyenne) */
@layer components {
.card { padding: 1rem; border: 1px solid #e2e8f0; }
.btn { padding: 0.5rem 1rem; border-radius: 8px; }
}
/* Couche 3 : Utilities (priorité la plus haute) */
@layer utilities {
.hidden { display: none !important; }
.text-center { text-align: center; }
}
/* Les styles non-couchés ont la priorité la plus haute */
.unlayered {
color: red; /* Écrase tout ce qui est dans les couches */
}
/* Ordre = Priorité : la dernière couche déclare gagne */
@layer reset, base, components, utilities;
/* Même si .card a une spécificité plus élevée,
utilities gagne toujours car c'est la dernière couche */
/* Importer des bibliothèques dans des couches */
@layer reset, third-party, my-components, my-utilities;
@import url('normalize.css') layer(reset);
@import url('some-lib.css') layer(third-party);
@layer my-components {
/* Vos composants — plus prioritaire que la lib */
}
@layer my-utilities {
/* Vos utilities — priorité maximale */
}
/* Définir un conteneur */
.card-wrapper {
container-type: inline-size; /* Réagit à la largeur */
container-name: card; /* Nom pour le ciblage */
}
/* ou avec le raccourci */
.card-wrapper {
container: card / inline-size;
}
/* Styles conditionnels basés sur le conteneur */
@container card (min-width: 400px) {
.card {
flex-direction: row; /* horizontal si le conteneur est large */
}
.card__image {
width: 200px;
}
}
@container card (max-width: 399px) {
.card {
flex-direction: column; /* vertical si le conteneur est étroit */
}
}
/* Container query units */
@container card (min-width: 400px) {
.card__title {
font-size: 2cqi; /* 2% de la largeur du conteneur */
}
}
/* inline-size — réagit à la largeur (le plus courant) */
.card-container { container-type: inline-size; }
/* size — réagit à la largeur ET la hauteur */
.full-container { container-type: size; }
/* Conteneur normal — pas de containment */
.parent { container-type: normal; }
/* cqi — container query inline (largeur) */
/* cqb — container query block (hauteur) */
/* cqmin — le plus petit des deux */
/* cqmax — le plus grand des deux */
.titre {
font-size: 3cqi; /* 3% de la largeur du conteneur */
line-height: 1.2cqb; /* 120% de la hauteur du conteneur */
}
:has() est le "sélecteur parent" tant attendu. Il permet de cibler un élément en fonction du contenu de ses enfants. C'est l'un des ajouts les plus puissants à l'histoire du CSS.
/* Cibler un formulaire qui contient un input en erreur */
.form:has(.input--error) {
border-color: red;
}
/* Cibler une carte qui contient une image */
.card:has(.card__image) {
display: grid;
grid-template-columns: 200px 1fr;
}
/* Cibler un élément qui suit un autre */
h2:has(+ p) {
/* h2 suivi immédiatement d'un paragraphe */
margin-bottom: 0.5rem;
}
/* Cibler un parent quand un enfant est focusé */
.form-group:has(input:focus) {
background: #eff6ff;
}
/* Conditions complexes */
.page:has(.sidebar):has(.main-content) {
display: grid;
grid-template-columns: 250px 1fr;
}
Le fond du groupe d'input change bleu quand l'input est focusé, grâce à :has(input:focus).
/* Le parent change d'apparence quand l'enfant est focusé */
.form-group {
transition: background 0.2s, padding 0.2s;
}
.form-group:has(input:focus) {
background: #eff6ff;
padding: 0.5rem;
margin: -0.5rem;
border-radius: 10px;
}
/* Le label change de couleur quand l'input n'est pas vide */
.form-group:has(input:not(:placeholder-shown)) label {
color: #2563eb;
font-weight: 700;
}
@scope crée un scope CSS — un domain de styles limité à un sous-ensemble du DOM. Les styles ne "fuient" pas du scope et ne peuvent pas être affectés par des styles extérieurs. C'est la modularité parfaite.
/* Définir un scope avec limites */
@scope (.card) to (.card__footer) {
/* Ces styles ne s'appliquent qu'aux éléments
inside .card mais PAS dans .card__footer */
p { color: #475569; }
a { color: #2563eb; text-decoration: underline; }
}
/* Les liens dans .card__footer ne seront PAS bleus */
/* Les liens dans .card__body seront bleus */
/* Scope sans limite basse — tout le sous-arbre */
@scope (.sidebar) {
.nav-link { color: white; }
/* S'applique à TOUS les .nav-link dans .sidebar */
}
/* Scope multiple */
@scope (.theme-a) {
/* Styles uniquement pour les éléments dans .theme-a */
button { background: blue; }
}
@scope (.theme-b) {
/* Styles uniquement pour les éléments dans .theme-b */
button { background: red; }
}
left/right et top/bottom par des concepts de block (vertical) et inline (horizontal), rendant le CSS nativement compatible avec les langues s'écrivant de droite à gauche (arabe, hébreu).
/* AVANT — Propriétés physiques */
.el {
margin-left: 1rem;
margin-right: 2rem;
padding-top: 0.5rem;
padding-bottom: 1rem;
border-left: 3px solid blue;
text-align: left;
float: right;
}
/* APRÈS — Propriétés logiques */
.el {
margin-inline-start: 1rem; /* gauche en LTR, droite en RTL */
margin-inline-end: 2rem;
padding-block-start: 0.5rem; /* haut */
padding-block-end: 1rem; /* bas */
border-inline-start: 3px solid blue;
text-align: start;
float: inline-end;
}
/* Raccourcis */
.el {
margin-inline: 1rem 2rem; /* start et end */
margin-block: 0.5rem 1rem; /* start et end */
padding-inline: 1rem; /* les deux côtés */
padding-block: 0.5rem; /* haut et bas */
inset-inline: 0; /* left + right en position absolute */
inset-block: 0; /* top + bottom */
inset: 0; /* raccourci pour les 4 */
}
CORRESPONDANCE PHYSIQUE → LOGIQUE ════════════════════════════════════ PHYSIQUE LOGIQUE DESCRIPTION ──────────────── ────────────────── ────────────────── margin-left margin-inline-start Début inline margin-right margin-inline-end Fin inline margin-top margin-block-start Début block margin-bottom margin-block-end Fin block padding-left padding-inline-start padding-right padding-inline-end padding-top padding-block-start padding-bottom padding-block-end border-left border-inline-start border-right border-inline-end border-top border-block-start border-bottom border-block-end left (position) inset-inline-start right (position) inset-inline-end top (position) inset-block-start bottom (position) inset-block-end width inline-size height block-size min-width min-inline-size max-width max-inline-size text-align: left text-align: start text-align: right text-align: end
Guide de correspondance des propriétés physiques vers logiques
/* Conteneur — type de snap */
.scroll-container {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory; /* ou x proximity */
gap: 1rem;
padding: 1rem;
}
/* Éléments — où snap */
.snap-item {
flex: 0 0 300px;
scroll-snap-align: start; /* start, center, end */
}
/* Scroll smooth */
.smooth-scroll {
scroll-behavior: smooth;
}
/* Scroll snap vertical */
.vertical-snap {
overflow-y: auto;
scroll-snap-type: y mandatory;
height: 400px;
}
.vertical-snap section {
scroll-snap-align: start;
min-height: 100%;
}
accent-color définit la couleur d'accentuation pour les éléments de formulaire natifs (checkbox, radio, range, progress). Un seul style change la couleur de tous les contrôles.
/* Accent global */
:root {
accent-color: #2563eb;
}
/* Accent par élément */
.checkbox { accent-color: #16a34a; }
.radio { accent-color: #7c3aed; }
.progress { accent-color: #f59e0b; }
/* Avec une classe */
.theme-primary { accent-color: #2563eb; }
.theme-success { accent-color: #16a34a; }
.theme-danger { accent-color: #dc2626; }
color-scheme indique au navigateur les schémas de couleurs supportés, permettant de styliser les éléments natifs (scrollbar, form controls, etc.) pour le thème sombre automatiquement.
/* Déclarer les schémas supportés */
:root {
color-scheme: light dark;
}
/* Le navigateur adaptera automatiquement :
- La scrollbar
- Les contrôles de formulaire natifs
- Le sélecteur de date
- Le remplissage automatique
*/
/* Thème sombre global */
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
}
body {
background: #0f172a;
color: #f1f5f9;
}
}
/* Forcer un thème spécifique */
.force-light { color-scheme: light; }
.force-dark { color-scheme: dark; }
/* oklch(luminosité chroma hue) */
/* Luminosité : 0 (noir) à 1 (blanc) */
/* Chroma : 0 (gris) à ~0.4 (saturated) */
/* Hue : 0-360 (teinte en degrés) */
:root {
--blue: oklch(0.55 0.2 250);
--green: oklch(0.6 0.18 145);
--purple: oklch(0.5 0.2 300);
--red: oklch(0.55 0.2 25);
/* Plus facile de créer des nuances */
--blue-50: oklch(0.95 0.05 250);
--blue-100: oklch(0.9 0.08 250);
--blue-500: oklch(0.55 0.2 250);
--blue-900: oklch(0.25 0.15 250);
}
/* Comparaison avec HSL */
.hsl-blue { color: hsl(220, 80%, 50%); } /* Pas uniforme */
.oklch-blue { color: oklch(0.55 0.2 250); } /* Uniforme */
/* Mélanger 2 couleurs avec un pourcentage */
.btn-primary {
background: color-mix(in oklch, #2563eb 100%, black 20%);
/* Le bleu assombri de 20% */
}
.btn-primary:hover {
background: color-mix(in oklch, #2563eb 100%, black 40%);
/* Encore plus sombre */
}
/* Créer un fond semi-transparent */
.overlay {
background: color-mix(in srgb, #000 50%, transparent);
/* Noir à 50% d'opacité */
}
/* Mélanger des couleurs pour des dégradés */
.gradient {
background: color-mix(in oklch, blue 50%, red 50%);
/* Violet obtenu en mélangeant bleu et rouge */
}
/* Créer des variantes de couleur */
:root {
--primary: #2563eb;
--primary-light: color-mix(in oklch, var(--primary) 80%, white);
--primary-dark: color-mix(in oklch, var(--primary) 80%, black);
--primary-subtle: color-mix(in oklch, var(--primary) 10%, white);
}
@starting-style définit les styles initiaux d'un élément avant qu'il n'entre dans le DOM (ou devienne visible), permettant des animations d'entrée sans JavaScript.
/* Élément qui apparaît avec une animation */
.dialog {
opacity: 1;
transform: translateY(0);
transition: opacity 0.3s, transform 0.3s;
}
/* Styles quand l'élément apparaît pour la première fois */
@starting-style {
.dialog {
opacity: 0;
transform: translateY(20px);
}
}
/* Pour un popover */
[popover]:popover-open {
opacity: 1;
transform: scale(1);
transition: opacity 0.3s, transform 0.3s, display 0.3s allow-discrete;
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
/* Pour un élément qui change de display:none à display:block */
.tooltip {
display: block;
opacity: 1;
transition: opacity 0.2s, display 0.2s allow-discrete;
}
@starting-style {
.tooltip {
opacity: 0;
}
}
<!-- HTML — Popover natif -->
<button popovertarget="mon-popover">Ouvrir</button>
<div id="mon-popover" popover>
<h3>Titre du popover</h3>
<p>Contenu du popover...</p>
<button popovertarget="mon-popover" popovertargetaction="hide">
Fermer
</button>
</div>
<!-- Le popover est :
- masqué par défaut (display:none)
- positionné par rapport au trigger
- gère le focus automatiquement
- se ferme avec Escape
- a un ::backdrop -->
<!-- CSS pour le popover -->
<style>
[popover] {
padding: 1.5rem;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
max-width: 300px;
}
/* Animation d'entrée */
[popover]:popover-open {
opacity: 1;
transform: scale(1);
transition: opacity 0.2s, transform 0.2s,
display 0.2s allow-discrete;
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
</style>
Ceci est un popover géré par le navigateur. Appuyez sur Escape pour fermer.
/* Card moderne avec CSS Nesting natif */
.card {
background: var(--color-bg);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
transition: box-shadow 0.3s, transform 0.3s;
&:hover {
box-shadow: var(--shadow-lg);
transform: translateY(-2px);
}
&__image {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
}
&__body {
padding: var(--space-6);
}
&__title {
font-size: var(--text-xl);
font-weight: 700;
margin-bottom: var(--space-2);
color: var(--color-text);
&:hover {
color: var(--color-primary);
}
}
&__text {
color: var(--color-text-secondary);
line-height: 1.6;
margin-bottom: var(--space-4);
}
&__footer {
padding: var(--space-4) var(--space-6);
border-top: 1px solid var(--color-border);
display: flex;
gap: var(--space-3);
}
&--featured {
border-color: var(--color-primary);
box-shadow: 0 0 0 1px var(--color-primary);
.card__title {
color: var(--color-primary);
}
}
&--compact {
.card__body {
padding: var(--space-4);
}
}
/* Responsive via nesting de media query */
@media (min-width: 768px) {
.card__body {
padding: var(--space-8);
}
}
}
/* Layout adaptatif de carte */
.product-card-wrapper {
container-type: inline-size;
container-name: product;
}
.product-card {
display: flex;
flex-direction: column;
gap: 1rem;
padding: 1rem;
background: white;
border-radius: 12px;
border: 1px solid #e2e8f0;
&__image {
width: 100%;
aspect-ratio: 4/3;
object-fit: cover;
border-radius: 8px;
}
&__content {
flex: 1;
}
&__title {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
&__price {
font-size: 1.25rem;
font-weight: 700;
color: #2563eb;
}
&__btn {
width: 100%;
margin-top: 1rem;
}
}
/* Layout horizontal quand le conteneur est large */
@container product (min-width: 500px) {
.product-card {
flex-direction: row;
align-items: center;
}
.product-card__image {
width: 200px;
aspect-ratio: 1;
flex-shrink: 0;
}
.product-card__btn {
width: auto;
}
}
/* Layout compact pour les très petits conteneurs */
@container product (max-width: 250px) {
.product-card__title {
font-size: 0.9rem;
}
.product-card__price {
font-size: 1rem;
}
}
/* Thème moderne avec color-scheme et oklch */
:root {
color-scheme: light dark;
/* Palette oklch */
--color-primary: oklch(0.55 0.2 250);
--color-primary-hover: oklch(0.45 0.2 250);
--color-bg: oklch(0.98 0.005 250);
--color-text: oklch(0.2 0.02 250);
--color-surface: oklch(0.96 0.005 250);
--color-border: oklch(0.88 0.005 250);
/* Couleurs dérivées avec color-mix */
--color-primary-light: color-mix(in oklch, var(--color-primary) 15%, transparent);
--color-primary-dark: color-mix(in oklch, var(--color-primary) 80%, black);
}
/* Adaptation automatique en mode sombre */
@media (prefers-color-scheme: dark) {
:root {
--color-bg: oklch(0.15 0.01 250);
--color-text: oklch(0.9 0.01 250);
--color-surface: oklch(0.2 0.01 250);
--color-border: oklch(0.3 0.01 250);
--color-primary: oklch(0.65 0.2 250);
}
}
/* Layout RTL-ready avec Logical Properties */
.article {
padding-block: 2rem;
padding-inline: 1.5rem;
max-width: 700px;
margin-inline: auto;
}
.article__title {
font-size: 2rem;
margin-block-end: 1rem;
border-block-end: 2px solid var(--color-border);
padding-block-end: 0.5rem;
}
.article__meta {
display: flex;
gap: 1rem;
margin-block-end: 1.5rem;
color: var(--color-text-secondary);
font-size: 0.85rem;
}
.article__content {
font-size: 1.05rem;
line-height: 1.8;
}
.article__content p {
margin-block-end: 1.5rem;
}
.article__sidebar {
position: sticky;
inset-inline-end: 1rem;
inset-block-start: 1rem;
}
/* Fonctionne automatiquement en RTL */
html[dir="rtl"] .article {
/* Pas besoin de changer quoi que ce soit ! */
/* padding-inline gère left/right automatiquement */
}
<!-- HTML -->
<button popovertarget="menu">Menu</button>
<div id="menu" popover>
<nav>
<a href="#">Accueil</a>
<a href="#">Produits</a>
<a href="#">Contact</a>
</nav>
</div>
/* CSS */
[popover] {
padding: 1rem;
border: 1px solid var(--color-border);
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0,0,0,0.12);
}
[popover]:popover-open {
opacity: 1;
transform: translateY(0) scale(1);
transition:
opacity 0.3s ease,
transform 0.3s ease,
display 0.3s allow-discrete,
overlay 0.3s allow-discrete;
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
}
/* Le::backdrop pour l'overlay */
[popover]::backdrop {
background: rgba(0,0,0,0.2);
transition: opacity 0.3s, display 0.3s allow-discrete, overlay 0.3s allow-discrete;
}
[popover]:popover-open::backdrop {
opacity: 1;
}
@starting-style {
[popover]:popover-open::backdrop {
opacity: 0;
}
}
/* FAUX — pas de conteneur déclaré */
.card-wrapper { /* rien */ }
@container (min-width: 400px) { /* ne fonctionne pas */ }
/* CORRECT — déclarer le conteneur */
.card-wrapper {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { flex-direction: row; }
}
/* ATTENTION — size crée un containment en HAUTEUR aussi */
.el {
container-type: size; /* Peut casser la mise en page ! */
}
/* RECOMMANDÉ — la plupart du temps, seul inline-size est nécessaire */
.el {
container-type: inline-size; /* Sécuritaire */
}
/* FAUX — utilities en premier = écrasé */
@layer utilities, components, base;
/* CORRECT — du moins prioritaire au plus prioritaire */
@layer base, components, utilities;
/* FAUX — 4+ niveaux de nesting, illisible */
.page {
& .content {
& .sidebar {
& .widget {
& .title { }
}
}
}
}
/* CORRECT — max 2-3 niveaux */
.page {
.sidebar {
.widget-title { }
}
}
/* FAUX — display ne transitionne pas par défaut */
.tooltip {
transition: opacity 0.3s, display 0.3s;
}
/* CORRECT — ajouter allow-discrete */
.tooltip {
transition: opacity 0.3s, display 0.3s allow-discrete;
}
/* PAS PORTABLE — ne fonctionne pas en RTL */
.el { margin-left: 1rem; }
/* PORTABLE — fonctionne en LTR et RTL */
.el { margin-inline-start: 1rem; }
/* FAUX — le navigateur ne sait pas qu'on supporte dark */
@media (prefers-color-scheme: dark) {
body { background: #0f172a; }
/* Les contrôles natifs restent en mode clair ! */
}
/* CORRECT */
:root { color-scheme: light dark; }
@media (prefers-color-scheme: dark) {
body { background: #0f172a; }
/* Les contrôles natifs basculent en mode sombre */
}
| Besoin | Fonctionnalité CSS moderne |
|---|---|
| Imbriquer des sélecteurs | CSS Nesting natif |
| Contrôler la cascade entre libs | @layer |
| Composant responsive | Container Queries |
| Cibler un parent depuis un enfant | :has() |
| Limiter la portée des styles | @scope |
| Support RTL automatique | Logical Properties |
| Carrousel / galerie | Scroll Snap |
| Couleurs de formulaires | accent-color |
| Dark mode natif | color-scheme |
| Couleurs perceptuellement uniformes | oklch / oklab |
| Mélanger des couleurs | color-mix() |
| Animations d'entrée | @starting-style |
| Overlays sans JS | Popover API |
Défi 1 : Refactorer avec CSS Nesting
Prenez ce CSS classique et réécrivez-le avec le CSS Nesting natif :
/* AVANT */
.card { background: white; border-radius: 12px; }
.card__title { font-weight: 700; }
.card__text { color: #64748b; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.card--featured { border-color: #2563eb; }
/* APRÈS (indices) */
.card {
background: white;
border-radius: 12px;
&:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
&__title { font-weight: 700; }
&__text { color: #64748b; }
&--featured { border-color: #2563eb; }
}
Défi 2 : Créer un layout Container Query
Créez un composant carte qui s'adapte à son conteneur : vertical en dessous de 300px, horizontal au-dessus.
Défi 3 : @layer pour un design system
Organisez un design system en 3 couches : base (reset), components (card, btn), utilities (hidden, text-center). Testez que les utilities écrasent bien les components.
Défi 4 : :has() pour un formulaire
Créez un formulaire où le groupe d'inputs change d'apparence quand l'input est focusé, en utilisant uniquement :has().
Défi 5 : Scroll Snap carousel
Créez un carrousel horizontal avec Scroll Snap qui affiche 3 cartes visibles et s'arrête proprement sur chaque carte au scroll.
Projet 1 : Design system CSS moderne
Créez un design system utilisant uniquement les fonctionnalités CSS modernes :
oklch pour les couleurscolor-mix() pour dériver les nuances@layer pour la cascadeaccent-color pour les formulairescolor-scheme pour le dark modeProjet 2 : Galerie avec Scroll Snap
Créez une galerie photo avec :
@starting-style pour les animations d'entréeProjet 3 : Dashboard responsive
Créez un dashboard utilisant :
:has() pour les états de formulaire@layer pour séparer reset/components/utilities| Fonctionnalité | Syntaxe | Usage principal |
|---|---|---|
| CSS Nesting | & { } |
Imbriquer les sélecteurs |
| @layer | @layer base, components, utils; |
Contrôler la cascade |
| Container Queries | container-type: inline-size; |
Responsive par composant |
| :has() | .parent:has(.child) |
Sélecteur parent |
| @scope | @scope (.a) to (.b) { } |
Portée de styles |
| Logical Properties | margin-inline-start |
Support RTL |
| Scroll Snap | scroll-snap-type: x mandatory; |
Carrousels magnétiques |
| accent-color | accent-color: #2563eb; |
Couleurs formulaires natifs |
| color-scheme | color-scheme: light dark; |
Dark mode natif |
| oklch() | oklch(0.55 0.2 250) |
Couleurs perceptuellement uniformes |
| color-mix() | color-mix(in oklch, #000 20%, #fff) |
Mélanger des couleurs |
| @starting-style | @starting-style { .el { opacity: 0; } } |
Animations d'entrée |
| Popover API | popovertarget="id" |
Overlays natifs sans JS |
margin-left ?oklch et pourquoi l'utiliser ?@starting-style ?&::before { } ou &::after { }.container-type: inline-size;@layer base, components, utilities;:has() : .form:has(.input--error) { border-color: red; }margin-inline-start (gauche en LTR, droite en RTL).x mandatory — force l'alignement sur un point de snap.color-mix(in oklch, couleur1 50%, couleur2 50%).popovertarget="id-de-l-element"/* ============================================
CSS MODERNE — Cheat Sheet
============================================ */
/* --- CSS NESTING --- */
.card {
&:hover { }
&__title { }
&--featured { }
@media (min-width: 768px) { }
}
/* --- @LAYER --- */
@layer reset, base, components, utilities;
/* --- CONTAINER QUERIES --- */
.wrap { container-type: inline-size; container-name: card; }
@container card (min-width: 400px) { .card { } }
/* --- :HAS() --- */
.form:has(input:focus) { }
.card:has(.image) { }
li:has(+ li:hover) { }
/* --- LOGICAL PROPERTIES --- */
margin-inline-start /* gauche LTR / droite RTL */
margin-inline-end /* droite LTR / gauche RTL */
margin-block-start /* haut */
margin-block-end /* bas */
padding-inline /* left + right */
inset-inline /* left + right (position) */
inline-size /* width */
block-size /* height */
/* --- SCROLL SNAP --- */
.container { scroll-snap-type: x mandatory; }
.item { scroll-snap-align: start; }
/* --- ACCENT-COLOR --- */
:root { accent-color: #2563eb; }
/* --- COLOR-SCHEME --- */
:root { color-scheme: light dark; }
/* --- OKLCH --- */
--blue: oklch(0.55 0.2 250);
/* --- COLOR-MIX --- */
--darker: color-mix(in oklch, var(--c) 80%, black);
/* --- @STARTING-STYLE --- */
.el { opacity: 1; transition: opacity 0.3s; }
@starting-style { .el { opacity: 0; } }
/* --- POPOVER --- */
<div popover>...</div>
<button popovertarget="id">Toggle</button>
[popover]:popover-open { opacity: 1; }
@starting-style { [popover]:popover-open { opacity: 0; } }