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-size au lieu de size pour éviter les calculs de layout inutiles
  • Vérifier si overflow: hidden affecte 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

Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →

#CSS#容器查询#现代布局#前端#教程