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 style qui cible les caractéristiques d'un élément parent (par exemple, sa largeur ou sa hauteur) pour styliser ses enfants. Récemment, une mise à jour importante du polyfill a été publiée, coïncidant avec la prise en charge des navigateurs.

Dans cet article, vous allez découvrir le fonctionnement du polyfill, les défis qu'il permet de surmonter et les bonnes pratiques à suivre pour l'utiliser afin d'offrir une expérience utilisateur de qualité à vos visiteurs.

dans le détail

Transpilation

Lorsque l'analyseur CSS d'un navigateur rencontre une règle @ inconnue, comme la toute nouvelle règle @container, il la supprime simplement comme si elle n'avait jamais existé. Par conséquent, la première chose que doit faire le polyfill est de convertir une requête @container en élément qui ne sera pas supprimé.

La première étape de la transpilation consiste à convertir la règle @container de niveau supérieur en une requête @media. Cela garantit principalement que le contenu reste regroupé. (par exemple, lorsque vous utilisez des API CSSOM et que vous affichez la source CSS).

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

Avant les requêtes de conteneur, le CSS n'avait pas la possibilité 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 identifiant unique (par exemple, 123), qui permet de transformer chaque sélecteur de sorte qu'il ne s'applique que lorsque l'élément comporte un attribut cq-XYZ qui inclut 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 accroît sa spécificité. Avec la pseudo-classe, la condition supplémentaire peut être appliquée tout en préservant la spécificité d'origine. Pour comprendre pourquoi c'est crucial, prenons l'exemple suivant:

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

.card {
  color: red;
}

Compte tenu de ce code CSS, un élément avec la classe .card devrait toujours avoir la valeur color: red, car la règle ultérieure prévaudra toujours sur 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 offre une solution de contournement simple et sécurisée: vous pouvez accroître intentionnellement 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;
}

Cette approche présente un certain nombre d'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 cette valeur factice par le sélecteur d'attribut ayant la même spécificité. Pour éviter toute surprise, le polyfill utilise les deux sélecteurs: le sélecteur de source d'origine est utilisé pour déterminer si l'élément doit recevoir l'attribut polyfill, et le sélecteur transcompilé est utilisé pour le style.

Pseudo-éléments

Vous vous posez peut-être la question suivante: 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 comporter d'attributs définis, peuvent-ils être pris en charge ?

Les pseudo-éléments sont toujours liés à un élément réel dans le DOM, appelé élément d'origine. Lors de la transpilation, 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 ce dont vous avez besoin. Un conteneur n'est pas autorisé à modifier des éléments qui ne sont pas à l'intérieur de celui-ci (et un conteneur ne peut pas se trouver à l'intérieur de lui-même). Toutefois, sachez 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 toute règle #foo serait appliquée par erreur.

Pour corriger ce problème, le polyfill utilise en fait deux attributs: l'un qui ne peut être appliqué à un élément que par un parent, et l'autre qu'un élément peut s'appliquer à 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'appliquera jamais le premier attribut (cq-XYZ-A) à lui-même, le premier sélecteur ne correspondra 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 ce faire, l'unité est transformée en expression calc(...) à l'aide de propriétés personnalisées CSS. Le polyfill définit 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 la writing-mode diffère 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);

À présent, les blocs peuvent être transformés dans la propriété CSS personnalisée appropriée, comme précédemment.

Propriétés

Les requêtes de conteneur ajoutent également de nouvelles propriétés CSS, telles que 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 analyse. 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écouvertes, ce qui permet au polyfill de s'adapter parfaitement aux autres fonctionnalités CSS telles que @supports. Cette fonctionnalité constitue la base des bonnes pratiques d'utilisation du polyfill, présentées 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 les propriétés natives se comportent. Pour résoudre ce problème, le polyfill insère la règle suivante avant tout style d'utilisateur, ce qui garantit 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

Même s'il est prévu que la plupart des visiteurs exécutent rapidement un navigateur avec prise en charge intégrée des requêtes de conteneur, il est toujours important d'offrir une bonne expérience aux autres visiteurs.

Lors du chargement initial, de nombreuses opérations doivent être effectuées pour 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 transcompilées. Étant donné qu'aucune API ne permet d'accéder à la source brute d'une feuille de style externe, il peut être nécessaire de la récupérer de nouveau 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 simplifier l'expérience des visiteurs, le polyfill a été conçu pour donner la priorité au FID (First Input Delay) et au CLS (Cumulative Layout Shift), potentiellement au détriment du Largest Contentful Paint (LCP). Concrètement, le polyfill ne garantit pas que vos requêtes de conteneur seront évaluées avant le premier remplissage. Cela signifie que 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. Pour accéder à une démonstration complète de cette approche, cliquez ici.

Cette approche est recommandée pour plusieurs raisons:

  • Un chargeur CSS pur réduit les frais généraux pour les utilisateurs disposant de navigateurs plus récents, tout en fournissant des informations légères à 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 cessera d'être transmise, et votre contenu s'affichera.
  • Dans les navigateurs compatibles avec les requêtes de conteneur, la condition ne sera jamais remplie et la page s'affichera comme prévu dans la première application.

Conclusion

Si vous souhaitez utiliser des requêtes de conteneur dans des navigateurs plus anciens, essayez le polyfill. Si vous rencontrez un problème, n'hésitez pas à nous le signaler.

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

Remerciements

Image principale de Dan Cristian Pădureț sur Unsplash.