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 éléments géographiques d'un élément parent (par exemple, sa largeur ou sa hauteur) afin d'appliquer un style à ses enfants. Récemment, une mise à jour importante du polyfill a été publiée, coïncidant avec la prise en charge dans les navigateurs.

Dans cet article, vous découvrirez comment fonctionne le polyfill, les défis qu'il permet de surmonter et les bonnes pratiques d'utilisation pour 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 arobase 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 et la plus importante que doit faire le polyfill est de transpiler une requête @container dans un élément qui ne sera pas supprimé.

La première étape de la transpilation consiste à convertir la règle @container de premier niveau en requête @media. Cela permet principalement de s'assurer que le contenu reste regroupé. C'est par exemple le cas lorsque vous utilisez les API CSSOM et que vous consultez le code source CSS.

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

Avant les requêtes de conteneur, le CSS ne permettait pas à un auteur d'activer ou de désactiver arbitrairement des groupes de règles. Pour émuler ce comportement, les règles d'une requête conteneur doivent également être transformées. Chaque @container se voit attribuer 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'attributs 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 en quoi cela est crucial, prenons l'exemple suivant:

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

.card {
  color: red;
}

Avec ce code CSS, un élément avec la classe .card devrait toujours comporter 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é. Par conséquent, transpiler la première règle et inclure un sélecteur d'attribut supplémentaire sans :where(...) augmenterait la spécificité et entraînerait l'application incorrecte de color: blue.

Cependant, la pseudo-classe :where(...) est plutôt 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 augmenter intentionnellement la spécificité de vos règles en ajoutant manuellement un sélecteur de :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 dans le CSS source a été modifié. La différence de spécificité est donc explicitement visible. Elle sert également de documentation afin 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 remplace cet objet factice par le sélecteur d'attributs ayant la même spécificité. Pour éviter toute mauvaise surprise, le polyfill utilise les deux sélecteurs: le sélecteur de source d'origine permet de déterminer si l'élément doit recevoir l'attribut de polyfill, et le sélecteur transpilé est utilisé pour définir 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, 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:

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 serait non 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 des éléments qui ne sont pas qu'il contient (et un conteneur ne peut pas être à l'intérieur de lui-même), mais gardez à l'esprit 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 fait deux attributs: un qui ne peut être appliqué qu'à un élément par un parent, et un autre qu'un élément peut s'appliquer à lui-même. Ce dernier attribut est utilisé pour les sélecteurs qui ciblent les 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 a rempli les conditions du conteneur et l'a appliqué.

Unités relatives du conteneur

Les requêtes de conteneur comportent également quelques nouvelles unités que vous pouvez utiliser dans votre code CSS, comme cqw et cqh pour 1% de la largeur et de la hauteur (respectivement) du conteneur parent approprié le plus proche. Pour être compatibles, l'unité est transformée en expression calc(...) à l'aide des propriétés CSS personnalisées. Le polyfill définit les valeurs de ces propriétés à l'aide de 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 du bloc (respectivement). Ces options sont un peu plus complexes, car les axes d'intégration et de bloc 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);

Les blocs peuvent maintenant être transformés en propriété personnalisée CSS appropriée, comme auparavant.

Propriétés

Les requêtes de conteneur ajoutent également quelques nouvelles propriétés CSS, telles que container-type et container-name. Il est impossible d'utiliser des API telles que getComputedStyle(...) avec des propriétés inconnues ou non valides. Elles sont donc également transformées en propriétés personnalisées CSS après analyse. Si une propriété ne peut pas être analysée (par exemple, parce qu'elle contient une valeur non valide ou inconnue), elle reste simplement laissée seule 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 fonctionner correctement avec d'autres fonctionnalités CSS comme @supports. Cette fonctionnalité est à la base des bonnes pratiques d'utilisation du polyfill, comme expliqué 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, ce qui signifie par exemple que tout enfant de .card prend la valeur de --cq-XYZ-container-name et --cq-XYZ-container-type. Ce n'est pas du tout le comportement des propriétés natives. Pour résoudre ce problème, le polyfill insère la règle suivante avant tous les styles utilisateur, ce qui garantit que chaque élément reçoit les valeurs initiales, sauf si une autre règle est volontairement remplacée par une autre.

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

Bonnes pratiques

On s'attend à ce que la plupart des visiteurs utilisent des navigateurs avec prise en charge intégrée des requêtes de conteneur le plus tôt possible, mais il est toujours important d'offrir une bonne expérience aux visiteurs restants.

Lors du chargement initial, de nombreuses étapes doivent être réalisées avant que le polyfill ne 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'il n'existe aucune API permettant d'accéder à la source brute d'une feuille de style externe, vous devrez peut-être la relancer de manière asynchrone, idéalement uniquement à partir du cache du navigateur.

Si le polyfill ne résout pas ces problèmes avec soin, vos métriques Core Web Vitals risquent d'enregistrer une régression.

Pour vous aider à offrir une expérience agréable aux visiteurs, le polyfill a été conçu pour donner la priorité au FID (First Input Delay) et au Cumulative Layout Shift (CLS), potentiellement au détriment du LCP (Largest Contentful Paint). Concrètement, le polyfill ne garantit pas que vos requêtes de conteneur seront évaluées avant le premier rendu. Par conséquent, pour optimiser l'expérience utilisateur, 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 l'associer à une animation de chargement CSS pur, positionnée de manière absolue sur 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 permet de réduire les frais généraux pour les utilisateurs de navigateurs récents, tout en fournissant des informations plus légères aux utilisateurs de navigateurs plus anciens et sur des réseaux plus lents.
  • En combinant le positionnement absolu du chargeur avec visibility: hidden, vous évitez un décalage de mise en page.
  • Une fois le polyfill chargé, cette condition @supports cesse de se transmettre, et votre contenu s'affiche.
  • Dans les navigateurs intégrant une prise en charge des requêtes de conteneur, la condition ne sera jamais transmise. Par conséquent, la page s'affichera en premier, comme prévu.

Conclusion

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

Nous avons hâte de voir et d'expérimenter tout ce que vous allez créer grâce à cet outil.

Remerciements

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