Im Containerabfrage-Polyfill

Gerald Monaco
Gerald Monaco

Containerabfragen sind eine neue CSS-Funktion, mit der Sie eine Stillogik schreiben können, die auf Elemente eines übergeordneten Elements (z. B. Breite oder Höhe) ausgerichtet ist, um dessen untergeordnete Elemente zu gestalten. Vor Kurzem wurde ein großes Update für Polyfill veröffentlicht, das parallel zur Unterstützung von Browsern veröffentlicht wurde.

In diesem Beitrag erhältst du einen Einblick in die Funktionsweise von Polyfills, die Herausforderungen, die damit bewältigt werden können, und die Best Practices, mit denen du deinen Besuchern eine hervorragende Nutzererfahrung bieten kannst.

Details

Transpilation

Wenn der CSS-Parser in einem Browser auf eine unbekannte at-Regel stößt, z. B. die brandneue @container-Regel, wird sie einfach verworfen, als ob sie nie existieren würde. Daher muss der Polyfill als Erstes eine @container-Abfrage in etwas übertragen, das nicht verworfen wird.

Der erste Schritt der Transpilierung besteht darin, die übergeordnete @container-Regel in eine @media-Abfrage umzuwandeln. Dies stellt in erster Linie sicher, dass die Inhalte gruppiert bleiben. beispielsweise bei der Verwendung von CSSOM-APIs und beim Aufrufen der CSS-Quelle.

Vorher
@container (width > 300px) {
  /* content */
}
Nachher
@media all {
  /* content */
}

Vor Containerabfragen hatte ein Autor im Preisvergleichsportal keine Möglichkeit, Regelgruppen willkürlich zu aktivieren oder zu deaktivieren. Um dieses Verhalten mit Polyfills zu füllen, müssen auch die Regeln innerhalb einer Containerabfrage transformiert werden. Jeder @container erhält eine eigene eindeutige ID (z. B. 123), mit der die einzelnen Selektoren so transformiert werden, dass sie nur angewendet werden, wenn das Element ein cq-XYZ-Attribut mit dieser ID hat. Dieses Attribut wird zur Laufzeit vom Polyfill festgelegt.

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

Beachten Sie die Verwendung der Pseudoklasse :where(...). Durch das Hinzufügen eines zusätzlichen Attributselektors wird normalerweise die Spezifität des Selektors erhöht. Mit der Pseudoklasse kann die zusätzliche Bedingung unter Beibehaltung der ursprünglichen Spezifität angewendet werden. Warum das so wichtig ist, sehen Sie am folgenden Beispiel:

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

.card {
  color: red;
}

Aufgrund dieses CSS-Codes sollte ein Element mit der Klasse .card immer color: red haben, da die spätere Regel immer die vorherige Regel mit demselben Selektor und derselben Spezifität überschreibt. Das Transpilieren der ersten Regel und das Einfügen eines zusätzlichen Attributselektors ohne :where(...) würde daher die Spezifität erhöhen und dazu führen, dass color: blue fälschlicherweise angewendet wird.

Die Pseudoklasse :where(...) ist jedoch ziemlich neu. Für Browser, die diese Funktion nicht unterstützen, bietet der Polyfill eine sichere und einfache Behelfslösung: Sie können die Regeln absichtlich spezifizieren, indem Sie Ihren @container-Regeln manuell einen Dummy-:not(.container-query-polyfill)-Selektor hinzufügen:

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

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

.card {
  color: red;
}

Dies hat eine Reihe von Vorteilen:

  • Der Selektor im Quell-CSS hat sich geändert, sodass der Unterschied in der Spezifität explizit sichtbar ist. Dies dient auch als Dokumentation, damit Sie wissen, was betroffen ist, wenn Sie die Problemumgehung oder den Polyfill nicht mehr unterstützen müssen.
  • Die Spezifität der Regeln bleibt immer gleich, da sie durch die Polyfill nicht geändert wird.

Während der Transpilierung ersetzt das Polyfill dieses Dummy-Element durch einen Attributselektor mit derselben Spezifität. Um Überraschungen zu vermeiden, verwendet der Polyfill beide Selektoren: Der ursprüngliche Quellenselektor wird verwendet, um zu bestimmen, ob das Element das Polyfill-Attribut erhalten soll, und der transpilierte Selektor wird für die Gestaltung verwendet.

Pseudoelemente

Eine Frage, die Sie sich stellen könnten, lautet: Wenn ein Polyfill ein cq-XYZ-Attribut für ein Element festlegt, das die eindeutige Container-ID 123 enthält, wie können Pseudoelemente, für die keine Attribute festgelegt werden können, unterstützt werden?

Pseudoelemente sind immer an ein echtes Element im DOM gebunden, das als Ursprungselement bezeichnet wird. Während der Transpilierung wird stattdessen der bedingte Selector auf dieses echte Element angewendet:

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

Anstatt in #foo::before:where([cq-XYZ~="123"]) umgewandelt zu werden (was ungültig wäre), wird der bedingte Selektor an das Ende des ursprünglichen Elements, #foo, verschoben.

Das ist aber nicht alles, was wir brauchen. Ein Container darf keine Inhalte ändern, die nicht in einem Container enthalten sind (und ein Container kann nicht in sich selbst sein). Sie sollten jedoch berücksichtigen, dass genau das passiert, wenn #foo selbst das abgefragte Containerelement wäre. Das Attribut #foo[cq-XYZ] würde fälschlicherweise geändert und alle #foo-Regeln werden fälschlicherweise angewendet.

Um dies zu korrigieren, verwendet der Polyfill tatsächlich zwei Attribute: eines, das nur von einem übergeordneten Element auf ein Element angewendet werden kann, und eines, das ein Element auf sich selbst anwenden kann. Das letztere Attribut wird für Selektoren verwendet, die auf Pseudoelemente ausgerichtet sind.

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

Da in einem Container niemals das erste Attribut (cq-XYZ-A) auf sich selbst angewendet wird, stimmt der erste Selektor nur überein, wenn ein anderer übergeordneter Container die Containerbedingungen erfüllt und angewendet hat.

Relative Containereinheiten

Containerabfragen enthalten auch ein paar neue Einheiten, die Sie in Ihrem CSS verwenden können, z. B. cqw und cqh für 1% der Breite bzw. Höhe des nächstgelegenen passenden übergeordneten Containers. Dazu wird die Einheit mithilfe von benutzerdefinierten CSS-Eigenschaften in einen calc(...)-Ausdruck umgewandelt. Der Polyfill legt die Werte für diese Eigenschaften über Inline-Stile für das Containerelement fest.

Vorher
.card {
  width: 10cqw;
  height: 10cqh;
}
Nachher
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

Es gibt auch logische Einheiten, wie cqi und cqb für die Inline-Größe bzw. die Blockgröße. Dies ist etwas komplizierter, da die Inline- und Blockachsen vom writing-mode des Elements bestimmt werden, das die Einheit verwendet, und nicht vom abgefragten Element. Um dies zu unterstützen, wendet der Polyfill einen Inline-Stil auf jedes Element an, dessen writing-mode sich von seinem übergeordneten Element unterscheidet.

/* 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);

Jetzt können die Einheiten wie zuvor in die entsprechende benutzerdefinierte CSS-Eigenschaft umgewandelt werden.

Attribute

Containerabfragen fügen außerdem einige neue CSS-Eigenschaften wie container-type und container-name hinzu. Da APIs wie getComputedStyle(...) nicht mit unbekannten oder ungültigen Eigenschaften verwendet werden können, werden diese nach dem Parsen auch in benutzerdefinierte CSS-Eigenschaften umgewandelt. Wenn eine Eigenschaft nicht geparst werden kann (z. B. weil sie einen ungültigen oder unbekannten Wert enthält), bleibt sie für den Browser unverändert.

Vorher
.card {
  container-name: card-container;
  container-type: inline-size;
}
Nachher
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

Diese Eigenschaften werden immer dann transformiert, wenn sie gefunden werden, sodass der Polyfill mit anderen CSS-Funktionen wie @supports kompatibel ist. Diese Funktionalität bildet die Grundlage der Best Practices für die Verwendung von Polyfill, wie unten beschrieben.

Vorher
@supports (container-type: inline-size) {
  /* ... */
}
Nachher
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

Standardmäßig werden benutzerdefinierte CSS-Eigenschaften übernommen. Das bedeutet, dass beispielsweise jedes untergeordnete Element von .card den Wert von --cq-XYZ-container-name und --cq-XYZ-container-type annimmt. So verhalten sich die nativen Properties definitiv nicht. Um dieses Problem zu lösen, fügt der Polyfill die folgende Regel vor allen Nutzerstilen ein. So wird sichergestellt, dass jedes Element die Anfangswerte erhält, es sei denn, es wird absichtlich durch eine andere Regel überschrieben.

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

Best Practices

Es ist zu erwarten, dass die meisten Besucher eher früher als später Browser mit integrierter Unterstützung für Containerabfragen ausführen. Dennoch ist es wichtig, die Nutzererfahrung für die verbleibenden Besucher zu optimieren.

Während des anfänglichen Ladevorgangs muss noch einiges geschehen, bevor der Polyfill die Seite formatieren kann:

  • Der Polyfill muss geladen und initialisiert werden.
  • Stylesheets müssen geparst und transpiliert werden. Da keine APIs für den Zugriff auf die Rohquelle eines externen Stylesheets vorhanden sind, muss sie möglicherweise asynchron erneut abgerufen werden, idealerweise jedoch nur aus dem Browser-Cache.

Wenn das Polyfill diese Bedenken nicht sorgfältig entkräftet, kann es zu einem Rückgang deiner Core Web Vitals kommen.

Damit du deinen Besuchern eine positive Erfahrung bieten kannst, wurden bei der Polyfill First Input Delay (FID) und Cumulative Layout Shift (CLS) priorisiert, was zu Kosten von Largest Contentful Paint (LCP) führen kann. Konkret garantiert der Polyfill nicht, dass Ihre Containerabfragen vor dem ersten Paint ausgewertet werden. Aus diesem Grund müssen Sie alle Inhalte, deren Größe oder Position sich durch Containerabfragen beeinflussen würde, so lange ausblenden, bis die Polyfill geladen und Ihr CSS-Code transpiliert wurde. Eine Möglichkeit dazu ist die Verwendung einer @supports-Regel:

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

Es wird empfohlen, dies mit einer reinen CSS-Ladeanimation zu kombinieren, die absolut über Ihrem (verborgenen) Content positioniert ist, um dem Besucher mitzuteilen, dass etwas passiert. Eine vollständige Demo dieses Ansatzes finden Sie hier.

Dieser Ansatz wird aus mehreren Gründen empfohlen:

  • Ein reiner CSS-Loader minimiert den Aufwand für Nutzer mit neueren Browsern und bietet Nutzern mit älteren Browsern und langsameren Netzwerken ein einfaches Feedback.
  • Durch die Kombination der absoluten Positionierung des Ladeprogramms mit visibility: hidden vermeiden Sie Layout Shifts.
  • Nachdem der Polyfill geladen wurde, wird die Bedingung @supports nicht mehr übergeben und deine Inhalte werden eingeblendet.
  • In Browsern mit integrierter Unterstützung für Containerabfragen wird die Bedingung nie erfüllt, sodass die Seite wie erwartet beim ersten Paint angezeigt wird.

Fazit

Wenn Sie Containerabfragen in älteren Browsern verwenden möchten, probieren Sie Polyfill aus. Falls Probleme auftreten sollten, können Sie das hier melden.

Wir sind gespannt, was Sie daraus machen werden.

Danksagungen

Hero-Image von Dan Cristian Pădureț auf Unsplash