Dans le polyfill de la requête de conteneur

Gerald Monaco
Gerald Monaco

Les requêtes de conteneur sont une nouvelle fonctionnalité CSS qui vous permet d'écrire une logique de stylisation qui cible les caractéristiques d'un élément parent (par exemple, sa largeur ou sa hauteur) pour styliser ses enfants. Récemment, une grande mise à jour du polyfill a été publiée, coïncidant avec la prise en charge dans les navigateurs.

Dans cet article, vous découvrirez le fonctionnement du polyfill, les défis qu'il surmonte et les bonnes pratiques à suivre pour offrir une expérience utilisateur optimale à vos visiteurs.

dans le détail

Transcompilation

Lorsque l'analyseur CSS d'un navigateur rencontre une règle at inconnue, comme la toute nouvelle règle @container, il la supprime comme si elle n'avait jamais existé. Par conséquent, la première et la plus importante tâche du polyfill consiste à transcompiler une requête @container en quelque chose qui ne sera pas supprimé.

La première étape de la transpilation consiste à convertir la règle @container de niveau supérieur en requête @media. Cela permet surtout de s'assurer que le contenu reste regroupé. Par exemple, lorsque vous utilisez les API CSSOM et lorsque vous consultez la source CSS.

Avant
@container (width > 300px) {
  /* content */
}
Après
@media all {
  /* content */
}

Avant les requêtes de conteneur, le CSS n'avait aucun moyen pour l'auteur d'activer ou de désactiver arbitrairement des groupes de règles. Pour émuler ce comportement, les règles contenues dans une requête de conteneur doivent également être transformées. Chaque @container reçoit son propre ID unique (par exemple, 123), qui permet de transformer chaque sélecteur afin qu'il ne s'applique que lorsque l'élément possède un attribut cq-XYZ incluant cet ID. Cet attribut sera défini par le polyfill au moment de l'exécution.

Avant
@container (width > 300px) {
  .card {
    /* ... */
  }
}
Après
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

Notez l'utilisation de la pseudo-classe :where(...). Normalement, l'ajout d'un sélecteur d'attribut supplémentaire augmente la spécificité du sélecteur. Avec la pseudo-classe, la condition supplémentaire peut être appliquée tout en préservant la spécificité d'origine. Pour comprendre pourquoi cela est crucial, prenons l'exemple suivant:

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

Avec ce CSS, un élément de la classe .card doit toujours avoir color: red, car la règle ultérieure remplacerait toujours la règle précédente avec le même sélecteur et la même spécificité. Transpiler la première règle et inclure un sélecteur d'attribut supplémentaire sans :where(...) augmenterait donc la spécificité et entraînerait l'application erronée de color: blue.

Cependant, la pseudo-classe :where(...) est assez nouvelle. Pour les navigateurs qui ne le prennent pas en charge, le polyfill fournit une solution de contournement sûre et facile: vous pouvez intentionnellement augmenter la spécificité de vos règles en ajoutant manuellement un sélecteur :not(.container-query-polyfill) factice à vos règles @container:

Avant
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
Après
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

Cela présente plusieurs avantages:

  • Le sélecteur du CSS source a été modifié. La différence de spécificité est donc explicitement visible. Cela sert également de documentation pour que vous sachiez ce qui est affecté lorsque vous n'avez plus besoin de prendre en charge la solution de contournement ou le polyfill.
  • La spécificité des règles sera toujours la même, car le polyfill ne la modifie pas.

Lors de la transpilation, le polyfill remplacera cet élément factice par le sélecteur d'attribut avec la même spécificité. Pour éviter toute surprise, le polyfill utilise les deux sélecteurs: le sélecteur source d'origine permet de déterminer si l'élément doit recevoir l'attribut polyfill, et le sélecteur transpilé est utilisé pour le style.

Pseudo-éléments

Vous vous demandez peut-être: si le polyfill définit un attribut cq-XYZ sur un élément pour inclure l'ID de conteneur unique 123, comment les pseudo-éléments, qui ne peuvent pas avoir d'attributs définis, peuvent-ils être compatibles ?

Les pseudo-éléments sont toujours liés à un élément réel du DOM, appelé élément d'origine. Lors de la transcompilation, le sélecteur conditionnel est appliqué à cet élément réel à la place:

Avant
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
Après
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

Au lieu d'être transformé en #foo::before:where([cq-XYZ~="123"]) (ce qui ne serait pas valide), le sélecteur conditionnel est déplacé à la fin de l'élément d'origine, #foo.

Cependant, ce n'est pas tout. Un conteneur n'est pas autorisé à modifier quoi que ce soit qui ne se trouve pas dans celui-ci (et un conteneur ne peut pas se trouver dans lui-même). Toutefois, considérez que c'est exactement ce qui se passerait si #foo était lui-même l'élément de conteneur interrogé. L'attribut #foo[cq-XYZ] serait modifié par erreur, et toutes les règles #foo seraient appliquées par erreur.

Pour corriger ce problème, le polyfill utilise en réalité deux attributs: l'un ne peut être appliqué à un élément que par un parent, et l'autre peut être appliqué par un élément lui-même. Ce dernier attribut est utilisé pour les sélecteurs qui ciblent des pseudo-éléments.

Avant
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
Après
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

Étant donné qu'un conteneur n'applique jamais le premier attribut (cq-XYZ-A) à lui-même, le premier sélecteur ne correspond que si un conteneur parent différent remplit les conditions du conteneur et l'a appliqué.

Unités relatives du conteneur

Les requêtes de conteneur incluent également quelques nouvelles unités que vous pouvez utiliser dans votre CSS, comme cqw et cqh pour 1% de la largeur et de la hauteur (respectivement) du conteneur parent le plus proche. Pour les prendre en charge, l'unité est transformée en expression calc(...) à l'aide des propriétés CSS personnalisées. Le polyfill définira les valeurs de ces propriétés via des styles intégrés sur l'élément conteneur.

Avant
.card {
  width: 10cqw;
  height: 10cqh;
}
Après
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Il existe également des unités logiques, telles que cqi et cqb pour la taille intégrée et la taille de bloc (respectivement). Ceux-ci sont un peu plus compliqués, car les axes alignés et les axes de volume sont déterminés par le writing-mode de l'élément utilisant l'unité, et non par l'élément interrogé. Pour ce faire, le polyfill applique un style intégré à tout élément dont le writing-mode diffère de celui de son parent.

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

Les unités peuvent désormais être transformées en propriété CSS personnalisée appropriée, comme auparavant.

Propriétés

Les requêtes de conteneur ajoutent également quelques nouvelles propriétés CSS, comme container-type et container-name. Étant donné que les API telles que getComputedStyle(...) ne peuvent pas être utilisées avec des propriétés inconnues ou non valides, elles sont également transformées en propriétés personnalisées CSS après avoir été analysées. Si une propriété ne peut pas être analysée (parce qu'elle contient une valeur non valide ou inconnue, par exemple), elle reste simplement traitée par le navigateur.

Avant
.card {
  container-name: card-container;
  container-type: inline-size;
}
Après
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Ces propriétés sont transformées chaque fois qu'elles sont détectées, ce qui permet au polyfill de fonctionner correctement avec d'autres fonctionnalités CSS telles que @supports. Cette fonctionnalité est la base des bonnes pratiques d'utilisation du polyfill, comme indiqué ci-dessous.

Avant
@supports (container-type: inline-size) {
  /* ... */
}
Après
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

Par défaut, les propriétés personnalisées CSS sont héritées. Cela signifie, par exemple, que tout enfant de .card prendra la valeur de --cq-XYZ-container-name et --cq-XYZ-container-type. Ce n'est certainement pas ainsi que se comportent les propriétés natives. Pour résoudre ce problème, le polyfill insère la règle suivante avant les styles utilisateur, garantissant que chaque élément reçoit les valeurs initiales, sauf s'il est volontairement remplacé par une autre règle.

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

Bonnes pratiques

Bien que la plupart des visiteurs utilisent des navigateurs compatibles avec les requêtes de conteneur intégrées dans un délai relativement court, il est important de proposer une bonne expérience aux autres visiteurs.

Lors du chargement initial, de nombreuses opérations doivent être effectuées avant que le polyfill puisse mettre en page la page:

  • Le polyfill doit être chargé et initialisé.
  • Les feuilles de style doivent être analysées et transpilées. Étant donné qu'il n'existe aucune API permettant d'accéder à la source brute d'une feuille de style externe, il peut être nécessaire de la récupérer de manière asynchrone, mais idéalement uniquement à partir du cache du navigateur.

Si le polyfill ne résout pas soigneusement ces problèmes, il pourrait potentiellement régresser vos Core Web Vitals.

Pour vous permettre de proposer plus facilement une expérience agréable à vos visiteurs, le polyfill a été conçu pour donner la priorité au First Input Delay (FID) (Délai de première entrée) et au Cumulative Layout Shift (CLS) (Déplacement cumulé de la mise en page), au détriment potentiel du Largest Contentful Paint (LCP) (Plus grande peinture de contenu). Concrètement, le polyfill ne garantit pas que vos requêtes de conteneur seront évaluées avant la première peinture. Par conséquent, pour une expérience utilisateur optimale, vous devez vous assurer que tout contenu dont la taille ou la position serait affectée par l'utilisation de requêtes de conteneur est masqué jusqu'à ce que le polyfill ait chargé et transpilé votre CSS. Pour ce faire, vous pouvez utiliser une règle @supports:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

Nous vous recommandons de combiner ce comportement avec une animation de chargement CSS pure, positionnée de manière absolue par-dessus votre contenu (masqué) pour indiquer au visiteur qu'il se passe quelque chose. Vous trouverez une démonstration complète de cette approche sur cette page.

Cette approche est recommandée pour plusieurs raisons:

  • Un chargeur CSS pur réduit les coûts pour les utilisateurs disposant de navigateurs plus récents, tout en fournissant des commentaires légers à ceux qui utilisent des navigateurs plus anciens et des réseaux plus lents.
  • En combinant le positionnement absolu du chargeur avec visibility: hidden, vous évitez le décalage de mise en page.
  • Une fois le polyfill chargé, cette condition @supports ne sera plus transmise et votre contenu s'affichera.
  • Dans les navigateurs compatibles avec les requêtes de conteneur, la condition ne sera jamais remplie. La page s'affichera donc lors de la première peinture, comme prévu.

Conclusion

Si vous souhaitez utiliser des requêtes de conteneur dans d'anciens navigateurs, essayez le polyfill. N'hésitez pas à signaler un problème si vous rencontrez des difficultés.

Nous avons hâte de découvrir les merveilles que vous allez créer avec elle.