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 erfahren Sie, wie die polyfill funktioniert, welche Herausforderungen sie löst und welche Best Practices Sie bei der Verwendung beachten sollten, um Ihren Besuchern eine hervorragende Nutzererfahrung zu bieten.

Funktionsweise

Transpilation

Wenn der CSS-Parser in einem Browser auf eine unbekannte At-Rule-Regel stößt, z. B. die brandneue @container-Regel, wird sie verworfen, als hätte sie nie existiert. Daher muss die polyfill als Erstes und Wichtigstes eine @container-Abfrage in etwas umwandeln, das nicht verworfen wird.

Der erste Schritt bei der Transpilierung besteht darin, die @container-Regel der obersten Ebene in eine @media-Abfrage umzuwandeln. So bleiben die Inhalte in der Regel gruppiert. Das ist beispielsweise bei der Verwendung von CSSOM APIs und beim Aufrufen der CSS-Quelldatei der Fall.

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 zu polyfillen, müssen auch die Regeln in einer Containerabfrage transformiert werden. Jede @container erhält eine eigene eindeutige ID (z. B. 123). Anhand dieser ID wird jede Auswahl so transformiert, dass sie nur angewendet wird, 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 angewendet werden, während die ursprüngliche Spezifität beibehalten wird. Das folgende Beispiel verdeutlicht, warum das wichtig ist:

@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. Wenn Sie die erste Regel transpilieren und einen zusätzlichen Attribut-Selector ohne :where(...) einfügen, wird die Spezifität erhöht und color: blue wird fälschlicherweise angewendet.

Die Pseudoklasse :where(...) ist jedoch relativ neu. Für Browser, die es nicht unterstützen, bietet die Polyfill-Funktion eine sichere und einfache Lösung: Sie können die Spezifität Ihrer Regeln absichtlich erhöhen, 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;
}

Das hat mehrere Vorteile:

  • Der Selektor im Quell-CSS hat sich geändert, sodass der Unterschied in der Spezifität deutlich sichtbar ist. Dies dient auch als Dokumentation, damit Sie wissen, was betroffen ist, wenn Sie die Umgehung oder die 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 die polyfill diesen Dummy durch den Attribut-Selector 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.

Pseudo-Elemente

Eine Frage, die Sie sich vielleicht stellen, ist: Wenn die Polyfill ein cq-XYZ-Attribut für ein Element festlegt, um die eindeutige Container-ID 123 anzugeben, wie können Pseudoelemente unterstützt werden, für die keine Attribute festgelegt werden können?

Pseudoelemente sind immer an ein echtes Element im DOM gebunden, das als Quellelement 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 jedoch nicht alles. Ein Container darf nichts ändern, was sich nicht in ihm befindet (und ein Container kann sich nicht in sich selbst befinden). Das wäre aber genau das, was passieren würde, wenn #foo selbst das Containerelement wäre, das abgefragt wird. Das #foo[cq-XYZ]-Attribut würde fälschlicherweise geändert und alle #foo-Regeln würden fälschlicherweise angewendet.

Um dies zu korrigieren, verwendet die Polyfill-Funktion 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 ein Container das erste Attribut (cq-XYZ-A) niemals auf sich selbst anwendet, wird die erste Auswahl nur dann getroffen, wenn ein anderer übergeordneter Container die Containerbedingungen erfüllt und angewendet hat.

Relative Containereinheiten

Containerabfragen bieten auch einige 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 übergeordneten Containers. Dazu wird der Anzeigenblock mithilfe von benutzerdefinierten CSS-Eigenschaften in einen calc(...)-Ausdruck umgewandelt. Die polyfill legt die Werte für diese Eigenschaften über Inline-Styles auf dem 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- und Blockgröße. Diese sind etwas komplizierter, da die Inline- und Blockachsen durch die writing-mode des Elements bestimmt werden, das die Einheit verwendet, nicht des Elements, das abgefragt wird. Dazu wendet die polyfill einen Inline-Stil auf jedes Element an, dessen writing-mode sich von dem des übergeordneten Elements 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-Property umgewandelt werden.

Attribute

Containerabfragen bieten außerdem einige neue CSS-Eigenschaften wie container-type und container-name. 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 Property nicht geparst werden kann (z. B. weil sie einen ungültigen oder unbekannten Wert enthält), wird sie einfach dem Browser überlassen.

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 jedes Mal transformiert, wenn sie erkannt werden. So kann die Polyfill-Funktion mit anderen CSS-Funktionen wie @supports verwendet werden. Diese Funktion bildet die Grundlage der Best Practices für die Verwendung der Polyfill, die unten beschrieben werden.

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

Benutzerdefinierte CSS-Properties werden standardmäßig übernommen. Das bedeutet, dass beispielsweise alle untergeordneten Elemente von .card den Wert von --cq-XYZ-container-name und --cq-XYZ-container-type annehmen. Das ist bei den nativen Properties definitiv nicht der Fall. Um dieses Problem zu lösen, fügt die polyfill die folgende Regel vor alle Nutzerstile ein, damit jedes Element die ursprünglichen Werte erhält, es sei denn, sie werden von einer anderen Regel absichtlich überschrieben.

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

Best Practices

Es wird erwartet, dass die meisten Besucher früher oder später Browser mit integrierter Unterstützung für Containerabfragen verwenden. Es ist jedoch wichtig, dass Sie auch den verbleibenden Besuchern eine gute Nutzererfahrung bieten.

Beim ersten Laden muss viel passieren, bevor die polyfill das Layout der Seite erstellen kann:

  • Die 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 die Datei möglicherweise asynchron erneut abgerufen werden, idealerweise jedoch nur aus dem Browser-Cache.

Wenn diese Probleme nicht sorgfältig durch die Polyfill-Funktion behoben werden, kann dies zu einer Verschlechterung Ihrer Core Web Vitals führen.

Damit Sie Ihren Besuchern eine angenehme Nutzung ermöglichen können, wurde die polyfill so konzipiert, dass First Input Delay (FID) und Cumulative Layout Shift (CLS) priorisiert werden, möglicherweise auf Kosten von Largest Contentful Paint (LCP). Konkret kann die polyfill nicht garantieren, dass Ihre Containerabfragen vor dem ersten Paint ausgewertet werden. Für eine optimale Nutzererfahrung müssen Sie dafür sorgen, dass alle Inhalte, deren Größe oder Position durch die Verwendung von Containerabfragen beeinflusst werden, erst nach dem Laden und Transpilieren des CSS ausgeblendet werden. Eine Möglichkeit hierfür ist die Verwendung einer @supports-Regel:

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

Wir empfehlen, dies mit einer reinen CSS-Ladeanimation zu kombinieren, die absolut über Ihren (ausgeblendeten) Inhalten positioniert ist, um den Besuchern zu signalisieren, 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.
  • Wenn Sie die absolute Positionierung des Ladebildschirms mit visibility: hidden kombinieren, vermeiden Sie Layoutänderungen.
  • Nachdem die Polyfill geladen wurde, wird diese @supports-Bedingung nicht mehr erfüllt und Ihre Inhalte werden angezeigt.
  • In Browsern mit integrierter Unterstützung für Containerabfragen wird die Bedingung nie erfüllt. Daher wird die Seite wie erwartet beim ersten Mal gerendert.

Fazit

Wenn Sie Containerabfragen in älteren Browsern verwenden möchten, können Sie die Polyfill-Funktion ausprobieren. Wenn Probleme auftreten, kannst du ein Problem melden.

Wir sind schon sehr gespannt, was ihr damit alles erschafft.