Pourquoi reparler du nesting CSS aujourd’hui ?

Pendant longtemps, imbriquer des sélecteurs CSS signifiait utiliser Sass, Less ou Stylus. C’était pratique : on écrivait les styles d’un composant à un seul endroit, avec une structure visuellement proche du HTML. Mais cela ajoutait aussi une étape de compilation, des conventions spécifiques et parfois des sélecteurs beaucoup trop longs.

Le nesting CSS natif change la situation : il permet d’imbriquer certains sélecteurs directement en CSS standard, sans préprocesseur. Ce n’est pas une invitation à écrire du CSS profondément imbriqué. C’est plutôt un outil supplémentaire pour structurer des composants, clarifier les états, regrouper les variantes et améliorer la maintenance.

Si vous avez déjà travaillé sur une architecture CSS composée de tokens, composants, utilitaires et overrides, le nesting devient particulièrement intéressant lorsqu’il est combiné avec les cascade layers, les design tokens CSS et TypeScript ou les container queries.

Le problème que le nesting essaie de résoudre

Prenons une carte d’article assez classique :

<article class="card">
  <img class="card__image" src="cover.jpg" alt="" />
  <div class="card__content">
    <h2 class="card__title">Comprendre le nesting CSS</h2>
    <p class="card__description">Un exemple de composant structuré.</p>
    <a class="card__link" href="/articles/css-nesting-natif">Lire l’article</a>
  </div>
</article>

Sans nesting, on obtient souvent une suite de sélecteurs plats :

.card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-surface);
}

.card__image {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.card__content {
  padding: var(--space-4);
}

.card__title {
  margin: 0;
  font-size: var(--font-size-lg);
}

.card__link:hover {
  text-decoration-thickness: 0.15em;
}

Ce code est correct. Le problème apparaît lorsque le composant grossit : variantes, états interactifs, thème sombre, media queries, container queries, préférences utilisateur, etc. On finit par disperser les règles ou répéter constamment le préfixe du composant.

Le nesting permet de regrouper ces règles sans perdre la nature déclarative du CSS.

La syntaxe de base du nesting CSS

Le nesting repose souvent sur le caractère &, qui représente le sélecteur parent courant.

.card {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--color-surface);

  &__image {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
  }

  &__content {
    padding: var(--space-4);
  }

  &__title {
    margin: 0;
    font-size: var(--font-size-lg);
  }

  &__link:hover {
    text-decoration-thickness: 0.15em;
  }
}

Cette écriture est concise, mais attention : elle produit des sélecteurs comme .card__image, .card__content et .card__title. Le & ne signifie pas « enfant de ». Il reprend textuellement le sélecteur parent.

Pour cibler un élément descendant, on écrit plutôt :

.card {
  background: var(--color-surface);

  & img {
    max-width: 100%;
  }

  & a {
    color: var(--color-link);
  }
}

Ici, & img correspond à .card img. Cette distinction est importante, surtout si vous mélangez une convention BEM avec des sélecteurs descendants.

Structurer les états sans répéter les sélecteurs

Le nesting devient très utile pour les états d’un composant : hover, focus-visible, aria-expanded, disabled, data attributes, etc.

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 2.75rem;
  padding-inline: var(--space-4);
  border: 0;
  border-radius: var(--radius-md);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font: inherit;
  cursor: pointer;

  &:hover {
    background: var(--color-accent-hover);
  }

  &:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 3px;
  }

  &:disabled,
  &[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
  }

  &[data-variant="secondary"] {
    background: var(--color-surface-muted);
    color: var(--color-text);
  }
}

Cette structure rend le composant plus lisible : le style principal, ses états et ses variantes sont regroupés. Pour une équipe, cela limite les recherches dans plusieurs fichiers et réduit les oublis lors des refactorings.

C’est aussi une bonne occasion de rappeler que l’état visuel ne doit pas remplacer la sémantique. Pour un bouton désactivé, utilisez disabled lorsque c’est possible. Pour un composant plus complexe, aria-disabled="true" peut être pertinent, mais il implique aussi de gérer le comportement JavaScript correctement. Sur les sujets d’accessibilité, les mêmes principes que ceux abordés dans les formulaires accessibles s’appliquent : le CSS doit accompagner l’interface, pas masquer une structure fragile.

Nesting et media queries

Le nesting ne sert pas uniquement à imbriquer des sélecteurs. Il permet aussi de placer des règles conditionnelles près du composant concerné.

.article-layout {
  display: grid;
  gap: var(--space-6);

  @media (width >= 48rem) {
    grid-template-columns: minmax(0, 1fr) 18rem;
    align-items: start;
  }

  @media (prefers-reduced-motion: reduce) {
    scroll-behavior: auto;
  }
}

Cette écriture évite de maintenir une grande section responsive en bas du fichier. Le comportement adaptatif reste localisé près de la règle de base.

Avec les container queries, l’approche devient encore plus intéressante :

.card-list {
  container-type: inline-size;
}

.card {
  display: grid;
  gap: var(--space-4);

  @container (width >= 36rem) {
    grid-template-columns: 14rem minmax(0, 1fr);
  }
}

Ici, la carte ne dépend pas de la largeur globale de la fenêtre, mais de la largeur disponible dans son conteneur. Pour des composants réellement réutilisables, cette approche est souvent plus robuste qu’une cascade de media queries globales.

Éviter le piège de l’imbrication profonde

Le nesting peut rapidement produire du CSS difficile à maintenir si l’on reproduit toute la structure HTML dans les sélecteurs.

À éviter :

.dashboard {
  & .panel {
    & .panel-header {
      & .actions {
        & button {
          & span {
            color: var(--color-muted);
          }
        }
      }
    }
  }
}

Ce type de code est fragile. Il dépend trop fortement de la structure du DOM. Le moindre changement de balisage peut casser le style. Il augmente aussi la spécificité, ce qui rend les surcharges plus difficiles.

Préférez des classes explicites et une profondeur limitée :

.panel {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);

  &__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
  }

  &__action-label {
    color: var(--color-muted);
  }
}

Une règle simple fonctionne bien en pratique : si vous dépassez deux niveaux d’imbrication, demandez-vous si le sélecteur exprime une intention métier ou seulement la forme actuelle du HTML.

Combiner nesting et cascade layers

Le nesting organise les règles localement. Les cascade layers organisent les priorités globales. Les deux répondent à des problèmes différents, mais se complètent très bien.

@layer reset, tokens, base, components, utilities;

@layer components {
  .alert {
    padding: var(--space-4);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    background: var(--color-surface-warning);

    & strong {
      font-weight: 700;
    }

    &[data-tone="danger"] {
      background: var(--color-surface-danger);
      border-color: var(--color-danger);
    }
  }
}

@layer utilities {
  .hidden {
    display: none !important;
  }
}

Dans cet exemple, le nesting améliore la lisibilité du composant .alert, tandis que @layer components définit sa place dans l’architecture CSS générale. C’est particulièrement utile dans un design system, où l’on veut éviter que les composants, les utilitaires et les styles de pages se concurrencent de manière imprévisible.

Exemple avec React et CSS classique

Le nesting CSS n’est pas réservé aux projets sans framework. Il s’intègre très bien dans une application React, Vue, Nuxt ou Next, tant que votre chaîne d’outillage accepte le CSS moderne ou le transpile correctement si nécessaire.

type NoticeProps = {
  title: string;
  children: React.ReactNode;
  tone?: "info" | "success" | "warning";
};

export function Notice({ title, children, tone = "info" }: NoticeProps) {
  return (
    <aside className="notice" data-tone={tone}>
      <h2 className="notice__title">{title}</h2>
      <div className="notice__body">{children}</div>
    </aside>
  );
}
.notice {
  padding: var(--space-4);
  border-radius: var(--radius-lg);
  border: 1px solid var(--color-border);
  background: var(--color-surface-info);

  &[data-tone="success"] {
    background: var(--color-surface-success);
  }

  &[data-tone="warning"] {
    background: var(--color-surface-warning);
  }

  &__title {
    margin-block: 0 var(--space-2);
    font-size: var(--font-size-md);
  }

  &__body {
    line-height: 1.6;
  }

  &__body > :last-child {
    margin-bottom: 0;
  }
}

Cette approche reste simple : le composant React porte l’état via data-tone, et le CSS déclare les variantes visuelles correspondantes. On évite de construire dynamiquement une longue liste de classes uniquement pour gérer trois variantes.

Bonnes pratiques pour utiliser le nesting CSS

Le nesting CSS est efficace lorsqu’il sert la lisibilité. Il devient problématique lorsqu’il cache une complexité de sélecteurs. Dans mes formations, je recommande généralement ces règles :

  • limitez l’imbrication à un ou deux niveaux ;
  • utilisez & pour les états, variantes et conventions de nommage explicites ;
  • évitez de reproduire toute la structure HTML dans le CSS ;
  • combinez le nesting avec des classes de composants lisibles ;
  • surveillez la spécificité générée ;
  • gardez les règles responsive proches du composant lorsqu’elles lui sont propres ;
  • organisez les priorités globales avec @layer plutôt qu’avec des sélecteurs plus longs.

Le point important est de ne pas confondre proximité et dépendance. Regrouper les styles d’un composant est utile. Rendre ces styles dépendants de cinq niveaux de DOM ne l’est pas.

Faut-il encore utiliser Sass ?

Sass ne disparaît pas parce que le CSS gagne le nesting. Sass fournit encore des fonctions, des boucles, des mixins et des outils d’organisation qui peuvent être utiles dans certains systèmes existants. Mais pour beaucoup de projets modernes, une partie de ce qui justifiait Sass est désormais disponible nativement : variables CSS, nesting, cascade layers, color functions, container queries, :has(), etc.

La bonne question n’est donc pas « Sass ou CSS natif ? », mais plutôt : quelle complexité êtes-vous prêt à ajouter à votre chaîne de build ? Si votre besoin principal est d’imbriquer proprement des états et variantes de composants, le nesting CSS natif suffit souvent.

Conclusion

Le nesting CSS natif est une amélioration discrète, mais importante. Il ne transforme pas CSS en langage de programmation et ne remplace pas une architecture réfléchie. En revanche, il rend les composants plus lisibles, réduit les répétitions et rapproche les règles conditionnelles du contexte auquel elles appartiennent.

Utilisé avec mesure, il s’intègre très bien dans une base CSS moderne aux côtés des design tokens, des cascade layers, des container queries et des conventions de composants. Comme souvent en CSS, la puissance de l’outil dépend surtout de la discipline avec laquelle vous l’utilisez.