CSS Container Queries & Layout Moderne en Pratique : Du Responsive au Component-Driven
L'Évolution du Design Responsive
Le design responsive CSS a évolué des media queries aux container queries. Les @media traditionnelles dépendent de la largeur de la fenêtre, tandis que @container répond à la taille du conteneur parent, permettant des composants responsifs véritablement autonomes.
| Étape | Technologie | Basé Sur | Limitation |
|---|---|---|---|
| Layout Fixe | Largeurs de pixel fixes | Aucune | Pas d'adaptation |
| Media Queries | @media |
Largeur du viewport | Les composants ne peuvent pas percevoir leur conteneur |
| Layout Flexible | Flexbox / Grid | Basé sur le contenu | Impossible de changer de layout par conteneur |
| Container Queries | @container |
Taille du conteneur parent | Véritable responsivité au niveau composant |
Syntaxe des Container Queries
Propriété container-type
.card-wrapper {
container-type: inline-size;
}
.sidebar-wrapper {
container-type: size;
}
.scroll-wrapper {
container-type: normal;
}
| Valeur | Signification | Dimensions Interrogeables |
|---|---|---|
inline-size |
Activer les requêtes sur l'axe inline | Largeur |
size |
Activer les requêtes sur les deux axes | Largeur + Hauteur |
normal |
Par défaut, pas de requêtes | Aucune |
container-name
.dashboard {
container-name: dashboard;
container-type: inline-size;
}
.sidebar {
container-name: sidebar;
container-type: inline-size;
}
Syntaxe Abrégée
.card-wrapper {
container: card / inline-size;
}
.sidebar-wrapper {
container: sidebar / inline-size;
}
Règles @container
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: 1rem;
}
}
@container card (min-width: 200px) and (max-width: 399px) {
.card {
display: flex;
flex-direction: column;
}
}
@container sidebar (min-height: 500px) {
.sidebar-nav {
position: sticky;
top: 1rem;
}
}
Unités de Container Query
.card-title {
font-size: clamp(1rem, 5cqi, 2rem);
padding: 2cqi;
}
.card-body {
margin-inline: 3cqi;
}
| Unité | Signification |
|---|---|
cqw |
1% de la largeur du conteneur |
cqh |
1% de la hauteur du conteneur |
cqi |
1% de la taille inline du conteneur |
cqb |
1% de la taille block du conteneur |
cqmin |
min(cqi, cqb) |
cqmax |
max(cqi, cqb) |
Pratique : Composant Card
<div class="card-wrapper">
<article class="card">
<img class="card-image" src="photo.jpg" alt="Exemple" />
<div class="card-content">
<h3 class="card-title">Container Queries en Action</h3>
<p class="card-desc">Changement automatique de layout en fonction de la largeur du conteneur parent</p>
<button class="card-btn">En Savoir Plus</button>
</div>
</article>
</div>
.card-wrapper {
container: card / inline-size;
}
.card {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
border-radius: 12px;
background: #fff;
box-shadow: 0 2px 8px rgb(0 0 0 / 0.1);
}
.card-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
border-radius: 8px;
}
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
align-items: center;
}
.card-image {
aspect-ratio: 1;
border-radius: 12px;
}
.card-title {
font-size: 1.25rem;
}
}
@container card (min-width: 600px) {
.card {
grid-template-columns: 280px 1fr;
padding: 1.5rem;
}
.card-title {
font-size: 1.5rem;
}
}
Pratique : Layout Sidebar
.page-layout {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.sidebar-region {
container: sidebar / inline-size;
}
@container sidebar (min-width: 280px) {
.sidebar-nav {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
}
@media (min-width: 768px) {
.page-layout {
grid-template-columns: 260px 1fr;
}
}
Pratique : Widgets de Tableau de Bord
.widget-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.widget {
container: widget / inline-size;
padding: 1rem;
border-radius: 12px;
background: #f8fafc;
}
@container widget (min-width: 350px) {
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.widget-chart {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
}
@container widget (max-width: 349px) {
.widget-header {
text-align: center;
}
.widget-chart {
display: flex;
flex-direction: column;
}
}
Sélecteur CSS :has() en Pratique
.form-group:has(input:invalid) {
border-color: #ef4444;
}
.form-group:has(input:invalid) .error-msg {
display: block;
}
.card:has(img) {
grid-template-rows: auto 1fr;
}
nav:has(.submenu:hover) .submenu {
opacity: 1;
visibility: visible;
}
details:has([open]) .arrow-icon {
transform: rotate(180deg);
}
.fieldset:has(:focus) {
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.3);
}
Imbrication Native CSS
.card {
padding: 1rem;
border-radius: 12px;
& .card-title {
font-size: 1.25rem;
font-weight: 700;
}
& .card-body {
margin-top: 0.5rem;
& p {
line-height: 1.6;
}
}
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 0.12);
}
@container card (min-width: 400px) {
display: grid;
grid-template-columns: 200px 1fr;
}
@media (prefers-color-scheme: dark) {
background: #1e293b;
color: #e2e8f0;
}
}
Subgrid pour l'Alignement
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.card {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
}
.card-title {
font-size: 1.125rem;
}
.card-body {
flex: 1;
}
.card-footer {
margin-top: auto;
}
CSS Anchor Positioning
.tooltip-trigger {
anchor-name: --trigger-anchor;
}
.tooltip {
position-anchor: --trigger-anchor;
position-area: top center;
position-visibility: anchors-visible;
padding: 0.5rem 0.75rem;
background: #1e293b;
color: #f8fafc;
border-radius: 6px;
font-size: 0.875rem;
}
.popover-menu {
position-anchor: --trigger-anchor;
position-area: bottom center;
position-visibility: anchors-visible;
margin-top: 4px;
padding: 0.5rem 0;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgb(0 0 0 / 0.15);
}
View Transitions API
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: fade-out 0.2s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease-in;
}
@keyframes fade-out {
to { opacity: 0; transform: scale(0.98); }
}
@keyframes fade-in {
from { opacity: 0; transform: scale(1.02); }
}
.card-image {
view-transition-name: card-img;
}
::view-transition-old(card-img) {
animation: shrink 0.3s ease-out;
}
::view-transition-new(card-img) {
animation: grow 0.3s ease-in;
}
document.querySelector('.nav-link').addEventListener('click', async (e) => {
e.preventDefault();
const transition = document.startViewTransition(async () => {
await updateContent(e.target.href);
});
await transition.finished;
});
Animations Scroll-Driven
.progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: #3b82f6;
transform-origin: left;
animation: scale-x linear;
animation-timeline: scroll();
}
@keyframes scale-x {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.reveal-item {
opacity: 0;
transform: translateY(30px);
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes reveal {
to {
opacity: 1;
transform: translateY(0);
}
}
.parallax-bg {
animation: parallax-move linear;
animation-timeline: scroll();
}
@keyframes parallax-move {
from { background-position-y: 0%; }
to { background-position-y: 100%; }
}
Cascade Layers (@layer)
@layer reset, base, components, utilities;
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
}
@layer base {
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
color: #1e293b;
}
a {
color: #3b82f6;
text-decoration: none;
}
}
@layer components {
.btn {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 8px;
font-weight: 500;
}
.btn-primary {
background: #3b82f6;
color: #fff;
}
}
@layer utilities {
.text-center { text-align: center; }
.mt-4 { margin-top: 1rem; }
}
CSS Reset Moderne
@layer reset {
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
hanging-punctuation: first last;
}
body {
min-height: 100dvh;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
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;
}
:root {
color-scheme: light dark;
}
@media (prefers-reduced-motion: no-preference) {
:has(:target) {
scroll-behavior: smooth;
}
}
}
Propriétés Logiques
.card {
margin-block: 1rem;
margin-inline: auto;
padding-block: 1.5rem;
padding-inline: 1rem;
border-inline-start: 4px solid #3b82f6;
inset-block-start: 0;
inset-inline-start: 0;
max-inline-size: 800px;
block-size: auto;
}
.nav-link {
margin-inline-end: 1rem;
padding-block: 0.5rem;
padding-inline: 1rem;
border-block-end: 2px solid transparent;
}
.nav-link:hover {
border-block-end-color: #3b82f6;
}
| Propriété Physique | Propriété Logique | Description |
|---|---|---|
margin-top |
margin-block-start |
Début direction block |
margin-bottom |
margin-block-end |
Fin direction block |
margin-left |
margin-inline-start |
Début direction inline |
margin-right |
margin-inline-end |
Fin direction inline |
width |
inline-size |
Dimension inline |
height |
block-size |
Dimension block |
top |
inset-block-start |
Décalage direction block |
left |
inset-inline-start |
Décalage direction inline |
Support des Navigateurs (2026)
| Fonctionnalité | Chrome | Firefox | Safari | Statut |
|---|---|---|---|---|
| Container Queries | 105+ | 110+ | 16+ | ✅ Stable |
| Container Query Units | 105+ | 110+ | 16+ | ✅ Stable |
:has() |
105+ | 121+ | 15.4+ | ✅ Stable |
| CSS Native Nesting | 120+ | 117+ | 17.2+ | ✅ Stable |
| Subgrid | 117+ | 71+ | 16+ | ✅ Stable |
| Anchor Positioning | 125+ | ❌ | ❌ | ⚠️ Expérimental |
| View Transitions | 111+ | ❌ | 18+ | ⚠️ Partiel |
| Scroll-driven Animations | 115+ | ❌ | ❌ | ⚠️ Expérimental |
@layer |
99+ | 97+ | 15.4+ | ✅ Stable |
| Logical Properties | 69+ | 66+ | 12.1+ | ✅ Stable |
Stratégies d'Amélioration Progressive
@supports (container-type: inline-size) {
.card-wrapper {
container: card / inline-size;
}
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
}
@supports not (container-type: inline-size) {
@media (min-width: 768px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
}
if (CSS.supports('container-type', 'inline-size')) {
document.documentElement.classList.add('cq-supported');
}
Pièges Courants et Débogage
Piège 1 : Oublier container-type
.card-wrapper {
container-name: card;
}
@container card (min-width: 400px) {
.card { display: grid; }
}
Vous devez également définir container-type ou utiliser la syntaxe abrégée container: card / inline-size.
Piège 2 : Le conteneur ne peut pas s'interroger lui-même
.card {
container-type: inline-size;
}
@container (min-width: 400px) {
.card { display: grid; }
}
Les container queries n'affectent que les descendants, pas l'élément sur lequel container-type est défini.
Piège 3 : La taille du conteneur est 0
Si le conteneur n'a pas de largeur explicite (par exemple, un enfant flex avec width: fit-content), les container queries peuvent ne pas fonctionner. Assurez-vous que le conteneur a une taille déterminée.
Conseils de Débogage
- Chrome DevTools → Panneau Elements → chercher les marqueurs
@container - Utiliser
container-type: inline-sizeau lieu desizepour éviter les calculs de layout inutiles - Vérifier si
overflow: hiddenaffecte le calcul de la taille
Considérations de Performance
| Préoccupation | Recommandation |
|---|---|
| Nombre de conteneurs | Éviter les conteneurs profondément imbriqués, chacun ajoute une surcharge de requête |
| container-type | Préférer inline-size, size déclenche un calcul bidirectionnel |
| Breakpoints de requête | Se limiter à 3-5, éviter trop de branches conditionnelles |
| Portée du reflow | Les container queries n'affectent que le sous-arbre, portée plus petite que les media queries |
| :has() | Éviter les sélecteurs :has() complexes sur de grands ensembles d'éléments |
| Animations de défilement | animation-timeline: scroll() est 10x+ plus rapide que les écouteurs d'événements scroll |
| View Transitions | Les snapshots old/new consomment de la mémoire, limiter le nombre de view-transition-name sur les grandes pages |
FAQ
Q : Les container queries peuvent-elles remplacer les media queries ?
R : Pas entièrement. Les layouts au niveau de la page (navbar, grille globale) ont encore besoin de @media, tandis que la responsivité au niveau composant utilise @container. Utilisez les deux ensemble.
Q : Quelle est la différence entre les container queries et l'auto-sizing Flexbox/Grid ?
R : Flexbox/Grid sont basés sur le contenu (s'adaptent à l'espace disponible), tandis que les container queries sont basées sur des conditions (passent à un mode de layout différent quand l'espace est suffisant).
Q : :has() affecte-t-il les performances ?
R : Une utilisation simple (par exemple, :has(input:focus)) a un overhead minimal. Évitez les sélecteurs :has() chaînés complexes sur des milliers d'éléments.
Q : Quelle est la différence entre Subgrid et Grid régulier ?
R : Subgrid permet aux grilles enfants d'hériter des définitions de tracks de la grille parente, permettant l'alignement entre cartes. Les grilles enfants régulières sont indépendantes et ne peuvent pas s'aligner entre éléments.
Q : Quand puis-je utiliser Anchor Positioning ?
R : À partir de 2026, Chrome 125+ le supporte. Firefox et Safari sont encore en cours d'implémentation. Pour la production, utilisez des bibliothèques comme Floating UI comme fallback.
Outils Associés
- Codage/Décodage Base64 — Gérer les données d'images inline en CSS
- Formateur JSON — Formater les configurations CSS-in-JS
- Convertisseur de Couleur — Convertir entre les formats de couleur CSS
Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →