Aller au contenu

Chapitre 5 : Imbrication (Nesting) en Sass/SCSS

Objectifs pédagogiques

À l'issue de ce chapitre, vous serez capable de : - Utiliser l'imbrication pour structurer votre code Sass - Comprendre les avantages du nesting pour la lisibilité - Éviter les pièges de l'imbrication excessive - Maîtriser l'impact sur la spécificité CSS - Appliquer les bonnes pratiques du nesting


Introduction

L'imbrication (ou nesting) est l'une des fonctionnalités les plus puissantes et les plus intuitives de Sass. Elle vous permet d'imbriquer les sélecteurs CSS à l'intérieur les uns des autres, reflétant ainsi la structure hiérarchique HTML de votre page.

<!-- La structure HTML que nous voulons cibler -->
<nav>
  <ul>
    <li>
      <a href="#">Accueil</a>
    </li>
  </ul>
</nav>

En CSS traditionnel, vous devriez écrire :

/* CSS classique */
nav ul {
  list-style: none;
}
nav ul li {
  display: inline-block;
}
nav ul li a {
  text-decoration: none;
  color: white;
}

En Sass, grâce à l'imbrication :

// Sass avec imbrication
nav {
  ul {
    list-style: none;

    li {
      display: inline-block;

      a {
        text-decoration: none;
        color: white;
      }
    }
  }
}

1. Imbrication simple

1.1 Principes de base

L'imbrication fonctionne en plaçant un sélecteur à l'intérieur d'un autre sélecteur. Le sélecteur intérieur sera combiné avec le sélecteur extérieur.

// Syntaxe générale
 {
   {
    propriété: valeur;
  }
}

// Équivaut en CSS à :
// 父 子 { propriété: valeur; }

1.2 Exemple : Navigation

// Sass
.nav {
  background-color: #333;
  padding: 10px 0;

  ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
  }

  li {
    margin-right: 20px;
  }

  a {
    color: white;
    text-decoration: none;
    font-weight: bold;

    &:hover {
      color: #3498db;
    }
  }
}

CSS compilé :

.nav {
  background-color: #333;
  padding: 10px 0;
}

.nav ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
}

.nav li {
  margin-right: 20px;
}

.nav a {
  color: white;
  text-decoration: none;
  font-weight: bold;
}

.nav a:hover {
  color: #3498db;
}

Note : Observez que chaque sélecteur imbriqué est combiné avec son parent avec un espace, ce qui correspond à un descendant selector en CSS.


2. Exemples d'imbrication en profondeur

2.1 Carte (Card) complète

.card {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transition: transform 0.3s ease;

  &:hover {
    transform: translateY(-5px);
    box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
  }

  &__header {
    padding: 20px;
    border-bottom: 1px solid #eee;

    h2 {
      margin: 0;
      font-size: 1.5rem;
      color: #2c3e50;
    }

    .subtitle {
      margin: 5px 0 0;
      font-size: 0.875rem;
      color: #7f8c8d;
    }
  }

  &__body {
    padding: 20px;

    p {
      line-height: 1.6;
      color: #555;

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

  &__footer {
    padding: 15px 20px;
    background-color: #f8f9fa;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .btn {
      padding: 8px 16px;
      border-radius: 4px;
      border: none;
      cursor: pointer;
      font-weight: 600;

      &--primary {
        background-color: #3498db;
        color: white;

        &:hover {
          background-color: #2980b9;
        }
      }

      &--secondary {
        background-color: #ecf0f1;
        color: #333;

        &:hover {
          background-color: #ddd;
        }
      }
    }

    .meta {
      font-size: 0.75rem;
      color: #95a5a6;
    }
  }
}

CSS compilé (extrait) :

.card {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  transition: transform 0.3s ease;
}

.card:hover {
  transform: translateY(-5px);
  box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
}

.card__header {
  padding: 20px;
  border-bottom: 1px solid #eee;
}

.card__header h2 {
  margin: 0;
  font-size: 1.5rem;
  color: #2c3e50;
}

.card__header .subtitle {
  margin: 5px 0 0;
  font-size: 0.875rem;
  color: #7f8c8d;
}

.card__body {
  padding: 20px;
}

.card__body p {
  line-height: 1.6;
  color: #555;
}

.card__body p:last-child {
  margin-bottom: 0;
}

.card__footer {
  padding: 15px 20px;
  background-color: #f8f9fa;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.card__footer .btn {
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  font-weight: 600;
}

.card__footer .btn--primary {
  background-color: #3498db;
  color: white;
}

.card__footer .btn--primary:hover {
  background-color: #2980b9;
}

.card__footer .btn--secondary {
  background-color: #ecf0f1;
  color: #333;
}

.card__footer .btn--secondary:hover {
  background-color: #ddd;
}

.card__footer .meta {
  font-size: 0.75rem;
  color: #95a5a6;
}

2.2 Formulaire complet

.form {
  max-width: 600px;
  margin: 0 auto;
  padding: 30px;

  &__group {
    margin-bottom: 20px;

    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
      color: #333;
      font-size: 0.9rem;

      .required {
        color: #e74c3c;
        margin-left: 3px;
      }
    }

    input,
    textarea,
    select {
      width: 100%;
      padding: 12px 16px;
      border: 2px solid #e0e0e0;
      border-radius: 6px;
      font-size: 1rem;
      font-family: inherit;
      transition: border-color 0.3s ease, box-shadow 0.3s ease;
      box-sizing: border-box;

      &:focus {
        outline: none;
        border-color: #3498db;
        box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
      }

      &::placeholder {
        color: #aaa;
      }

      &.is-error {
        border-color: #e74c3c;

        &:focus {
          box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.2);
        }
      }

      &.is-success {
        border-color: #2ecc71;

        &:focus {
          box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
        }
      }
    }

    textarea {
      min-height: 120px;
      resize: vertical;
    }

    select {
      appearance: none;
      background-image: url("data:image/svg+xml,...");
      background-repeat: no-repeat;
      background-position: right 12px center;
      cursor: pointer;
    }
  }

  &__error {
    margin-top: 6px;
    font-size: 0.8rem;
    color: #e74c3c;
    display: flex;
    align-items: center;

    &::before {
      content: "⚠ ";
      margin-right: 5px;
    }
  }

  &__hint {
    margin-top: 6px;
    font-size: 0.8rem;
    color: #7f8c8d;
  }

  &__actions {
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 30px;
    padding-top: 20px;
    border-top: 1px solid #eee;
  }

  &--inline {
    display: flex;
    flex-wrap: wrap;
    gap: 15px;

    .form__group {
      flex: 1;
      min-width: 200px;
    }
  }
}

2.3 Grille responsive

.grid {
  display: grid;
  gap: 20px;

  &--2 {
    grid-template-columns: repeat(2, 1fr);
  }

  &--3 {
    grid-template-columns: repeat(3, 1fr);
  }

  &--4 {
    grid-template-columns: repeat(4, 1fr);
  }

  @media (max-width: 768px) {
    &--2,
    &--3,
    &--4 {
      grid-template-columns: 1fr;
    }
  }

  &__item {
    background: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

    &--featured {
      grid-column: span 2;
      border: 2px solid #3498db;
    }

    &--full {
      grid-column: 1 / -1;
    }
  }
}

3. Bonnes pratiques pour l'imbrication

3.1 Règle des 3 niveaux maximum

// ✅ BON : Imbrication raisonnable (2-3 niveaux)
.sidebar {
  padding: 20px;

  .widget {
    margin-bottom: 30px;

    &__title {
      font-size: 1.2rem;
      font-weight: bold;
      margin-bottom: 10px;
    }

    &__content {
      color: #555;
    }
  }
}

// ❌ MAUVAIS : Imbrication excessive (5+ niveaux)
// Cela génère une spécificité énorme et un CSS difficile à maintenir
.page {
  .content {
    .section {
      .article {
        .paragraph {
          color: red;  // Spécificité : .page .content .section .article .paragraph
        }
      }
    }
  }
}

3.2 Profondeur d'imbrication recommandée

// ✅ 1 niveau - Très bon
.container {
  max-width: 1200px;
  margin: 0 auto;

  .inner {
    padding: 20px;
  }
}

// ✅ 2 niveaux - Bon
.nav {
  background: #333;

  ul {
    display: flex;

    li {
      margin-right: 10px;
    }
  }
}

// ⚠️ 3 niveaux - Acceptable avec parcimonie
.form {
  padding: 20px;

  .group {
    margin-bottom: 15px;

    label {
      font-weight: bold;
    }
  }
}

// ❌ 4+ niveaux - À éviter
// Cherchez plutôt à restructurer votre code

3.3 Séparation des responsabilités

// ✅ BON : Chaque composant a sa propre structure
.button {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;

  &--primary {
    background: #3498db;
    color: white;
  }

  &--danger {
    background: #e74c3c;
    color: white;
  }

  &:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }
}

.alert {
  padding: 15px;
  border-radius: 4px;
  margin-bottom: 10px;

  &--success {
    background: #d4edda;
    color: #155724;
    border: 1px solid #c3e6cb;
  }

  &--error {
    background: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
  }
}

4. L'imbrication et la spécificité CSS

4.1 Comprendre la spécificité

La spécificité détermine quel style s'applique quand plusieurs règles matchent le même élément. Plus la spécificité est élevée, plus le style a de poids.

// Chaque niveau d'imbrication AJOUTE de la spécificité

// Spécificité : 0-1-0 (un seul sélecteur de classe)
.card { }

// Spécificité : 0-2-0 (deux sélecteurs de classe)
.card .title { }

// Spécificité : 0-3-0 (trois sélecteurs de classe)
.card .title span { }

// Spécificité : 0-4-0 (quatre sélecteurs de classe)
.card .title span strong { }

// Exemple concret de problème
.card {
  .title {
    color: blue;  // Spécificité : 0-2-0
  }
}

// Plus tard, on veut écraser cette couleur
.title {
  color: red;  // Spécificité : 0-1-0  NE FONCTIONNE PAS !
  // Le style .card .title a plus de poids
}

4.2 Problèmes concrets de spécificité

// Scénario problématique
.modal {
  .header {
    padding: 20px;

    h2 {
      color: #333;  // Spécificité : 0-3-0
    }
  }
}

// On veut réutiliser h2 sans imbrication
// Impossible d'écraser sans utiliser !important ou plus d'imbrication

// Solution : Limiter l'imbrication
.modal {
  // Properties directes du modal...
}

.modal__header {
  padding: 20px;
}

.modal__title {
  color: #333;  // Facile à écraser
}

// Ou utiliser BEM pour éviter les problèmes
.card {
  &__title {
    color: #333;  // .card__title  Spécificité : 0-1-0
  }
}

4.3 Impact sur la maintenance

// ❌ Code difficile à maintenir (spécificité élevée)
.page-wrapper {
  .main-content {
    .article-list {
      .article-item {
        .article-title {
          font-size: 1.5rem;  // Spécificité : 0-5-0
        }
      }
    }
  }
}

// Pour changer cette taille, il faudra :
// 1. Soit créer un sélecteur encore plus spécifique
// 2. Soit utiliser !important (mauvaise pratique)
// 3. Soit refactoriser tout le CSS

// ✅ Code facile à maintenir
.article-title {
  font-size: 1.5rem;  // Spécificité : 0-1-0
}

// Facile à écraser quand nécessaire
.article-title--large {
  font-size: 2rem;
}

5. Cas d'utilisation avancés

5.1 Thème sombre avec variables

$bg-primary: #ffffff;
$text-primary: #333333;
$bg-secondary: #f5f5f5;

.theme-dark {
  $bg-primary: #1a1a2e;
  $text-primary: #e0e0e0;
  $bg-secondary: #16213e;

  background-color: $bg-primary;
  color: $text-primary;

  .header {
    background-color: $bg-secondary;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);

    .logo {
      filter: invert(1);
    }
  }

  .sidebar {
    background-color: $bg-secondary;

    .link {
      color: $text-primary;

      &:hover {
        background-color: rgba(255, 255, 255, 0.1);
      }
    }
  }
}

5.2 Composant avec états multiples

.notification {
  padding: 16px 20px;
  border-radius: 8px;
  margin-bottom: 12px;
  display: flex;
  align-items: flex-start;
  gap: 12px;

  &--info {
    background-color: #e8f4fd;
    border-left: 4px solid #2196f3;
    color: #0d47a1;
  }

  &--success {
    background-color: #e8f5e9;
    border-left: 4px solid #4caf50;
    color: #1b5e20;
  }

  &--warning {
    background-color: #fff3e0;
    border-left: 4px solid #ff9800;
    color: #e65100;
  }

  &--error {
    background-color: #ffebee;
    border-left: 4px solid #f44336;
    color: #b71c1c;
  }

  &__icon {
    font-size: 1.5rem;
    line-height: 1;
    flex-shrink: 0;
  }

  &__content {
    flex: 1;

    &-title {
      font-weight: 600;
      margin-bottom: 4px;
    }

    &-message {
      font-size: 0.9rem;
      opacity: 0.9;
    }
  }

  &__close {
    background: none;
    border: none;
    font-size: 1.2rem;
    cursor: pointer;
    opacity: 0.6;
    transition: opacity 0.2s;

    &:hover {
      opacity: 1;
    }
  }

  // État d'animation
  &.is-entering {
    animation: slideIn 0.3s ease-out;
  }

  &.is-leaving {
    animation: slideOut 0.3s ease-in forwards;
  }
}

@keyframes slideIn {
  from {
    transform: translateX(-100%);
    opacity: 0;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

@keyframes slideOut {
  from {
    transform: translateX(0);
    opacity: 1;
  }
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}

5.3 Layout complexe

.layout {
  display: grid;
  grid-template-areas:
    "header header header"
    "sidebar main aside"
    "footer footer footer";
  grid-template-columns: 250px 1fr 200px;
  grid-template-rows: auto 1fr auto;
  min-height: 100vh;

  @media (max-width: 768px) {
    grid-template-areas:
      "header"
      "main"
      "footer";
    grid-template-columns: 1fr;
  }

  &__header {
    grid-area: header;
    background: #333;
    color: white;
    padding: 16px 24px;
    display: flex;
    justify-content: space-between;
    align-items: center;

    .logo {
      font-size: 1.5rem;
      font-weight: bold;
    }

    .nav {
      display: flex;
      gap: 20px;

      a {
        color: white;
        text-decoration: none;

        &:hover {
          color: #3498db;
        }
      }
    }

    @media (max-width: 768px) {
      flex-direction: column;
      gap: 12px;

      .nav {
        flex-wrap: wrap;
        justify-content: center;
      }
    }
  }

  &__sidebar {
    grid-area: sidebar;
    background: #f8f9fa;
    padding: 20px;
    border-right: 1px solid #dee2e6;

    @media (max-width: 768px) {
      display: none;
    }
  }

  &__main {
    grid-area: main;
    padding: 30px;
  }

  &__aside {
    grid-area: aside;
    background: #f8f9fa;
    padding: 20px;
    border-left: 1px solid #dee2e6;

    @media (max-width: 768px) {
      display: none;
    }
  }

  &__footer {
    grid-area: footer;
    background: #333;
    color: white;
    padding: 20px;
    text-align: center;
  }
}

6. Différence entre imbrication et sélecteurs combinés

6.1 Descendant vs Child

// Descendant selector (par défaut avec l'imbrication)
.nav {
  a {
    color: white;
  }
}
// Équivaut à : .nav a { color: white; }
// Cible TOUS les <a> descendants de .nav

// Pour le child selector (>), on utilise &
.nav {
  > a {
    color: white;
  }
}
// Équivaut à : .nav > a { color: white; }
// Cible uniquement les <a> enfants directs de .nav

6.2 Adjacent sibling

// Pour le sélecteur adjacent (+)
.content {
  & + & {
    margin-top: 20px;
  }
}
// Équivaut à : .content + .content { margin-top: 20px; }

6.3 General sibling

// Pour le sélecteur sibling (~)
.section {
  & ~ & {
    margin-top: 10px;
  }
}
// Équivaut à : .section ~ .section { margin-top: 10px; }

7. Alternatives à l'imbrication profonde

7.1 Utiliser BEM pour réduire l'imbrication

// ❌ Imbrication profonde
.page {
  .content {
    .sidebar {
      .widget {
        .title {
          font-weight: bold;
        }
      }
    }
  }
}

// ✅ Avec BEM - pas d'imbrication nécessaire
.page-content { }
.sidebar { }
.widget { }
.widget__title {
  font-weight: bold;
}

7.2 Utiliser des classes utilitaires

// Au lieu d'imbriquer pour cibler des cas spécifiques
// Utilisez des classes utilitaires

.text-center { text-align: center; }
.text-bold { font-weight: bold; }
.mt-1 { margin-top: 8px; }
.mt-2 { margin-top: 16px; }
.p-1 { padding: 8px; }

// Puis dans votre HTML :
// <h2 class="text-center text-bold mt-2">Titre</h2>

7.3 Utiliser des mixins pour factoriser

// Mixin pour les media queries
@mixin respond-to($breakpoint) {
  @if $breakpoint == 'sm' {
    @media (max-width: 576px) { @content; }
  } @else if $breakpoint == 'md' {
    @media (max-width: 768px) { @content; }
  } @else if $breakpoint == 'lg' {
    @media (max-width: 992px) { @content; }
  }
}

// Utilisation sans imbrication profonde
.card {
  padding: 20px;
}

@include respond-to('md') {
  .card {
    padding: 10px;
  }
}

8. Exercices pratiques

Exercice 1 : Navigation basique

Créez une navigation avec imbrication :

// Structure HTML :
// <nav class="main-nav">
//   <ul>
//     <li><a href="#">Accueil</a></li>
//     <li><a href="#">Services</a></li>
//     <li><a href="#">Contact</a></li>
//   </ul>
// </nav>

// Style souhaité :
// - nav : fond noir, padding
// - ul : display flex, pas de marge/padding
// - li : margin right
// - a : blanc, pas de soulignement
// - a:hover : couleur bleue

Exercice 2 : Carte produit

// Créez un composant .product-card avec :
// - Un conteneur avec ombre et bordure arrondie
// - .product-image (hauteur固定, overflow hidden)
// - .product-info avec :
//   - .product-name (gras, 1.2rem)
//   - .product-price (couleur verte, 1.5rem)
//   - .product-rating (étoiles)
// - .product-actions avec un bouton
// Limitez-vous à 3 niveaux d'imbrication maximum

Exercice 3 : Formulaire de contact

// Créez un formulaire avec imbrication :
// - .contact-form (max-width: 500px, margin auto)
// - .form-row (flex, gap)
// - .form-field (margin-bottom)
//   - label (display block, margin-bottom)
//   - input (width 100%, padding, border)
//   - input:focus (border-color bleu)
//   - .error-message (petit, rouge)
// - .submit-btn (pleine largeur, fond bleu)

Exercice 4 : Layout page

// Créez un layout complet :
// - .page-header (flex, space-between, padding)
//   - .logo
//   - .main-nav > ul > li > a
// - .page-content (flex, gap)
//   - .sidebar (width: 250px)
//   - .main-content (flex: 1)
//     - .article > h2 + p + .read-more
// - .page-footer (background sombre, text-center)
//     - .footer-links > a (margin)
//     - .copyright

Exercice 5 : Composant avec états

// Créez un bouton .toggle-switch avec :
// - Conteneur (relative, width, height)
// - .toggle-track (absolute, background gris)
// - .toggle-thumb (absolute, blanc, border-radius)
// - .toggle-track.is-active (background bleu)
// - .toggle-track.is-active + .toggle-thumb (transform translate)
// - États disabled et focus
// - Animation de transition

Exercice 6 : Analyse de spécificité

// Analysez cette imbrication et calculez la spécificité de chaque sélecteur :
.page {
  .content {
    .article {
      .title { font-size: 2rem; }
      .meta { color: gray; }
    }
  }
}

// Puis refactorisez pour réduire la spécificité
// tout en gardant le même rendu visuel

Résumé

Concept Description
Imbrication simple Sélecteur dans sélecteur
Profondeur max 2-3 niveaux recommandés
Spécificité Augmente avec chaque niveau
Descendant parent { child {} }
Child parent { > child {} }
Bonne pratique Utiliser BEM pour réduire l'imbrication
Alternative Classes utilitaires, mixins

Chapitre suivant

Dans le Chapitre 6 - Le parent selector (&), nous verrons comment utiliser le sélecteur parent & pour créer des variantes, des pseudo-classes et des combinaisons avancées.