Im Containerabfrage-Polyfill

Gerald Monaco
Gerald Monaco

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

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 Regel @container, 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 der Fall, wenn Sie CSSOM APIs verwenden und sich die CSS-Quelldatei ansehen.

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

Vor Containerabfragen gab es in CSS keine Möglichkeit, dass ein Autor Regelgruppen willkürlich aktivieren oder deaktivieren konnte. 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 von der polyfill zur Laufzeit festgelegt.

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

Beachten Sie die Verwendung der Pseudoklasse :where(...). Normalerweise würde die Spezifizität des Selektors durch einen zusätzlichen Attributselektor 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;
}

Bei diesem CSS 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 unerwartete Ergebnisse zu vermeiden, werden in der polyfill beide Auswahlen verwendet: Mit der ursprünglichen Quellauswahl wird ermittelt, ob das Element das polyfill-Attribut erhalten soll, und die transpilierte Auswahl wird für das Styling 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 die bedingte Auswahl stattdessen auf dieses 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 die bedingte Auswahl ans 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) nie 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 Properties verwendet werden können, werden diese nach dem Parsen ebenfalls in benutzerdefinierte CSS-Properties 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) {
  /* ... */
}

Standardmäßig werden benutzerdefinierte CSS-Properties vererbt. Das bedeutet, dass beispielsweise jedes untergeordnete Element von .card den Wert von --cq-XYZ-container-name und --cq-XYZ-container-type übernimmt. 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 absichtlich durch eine andere Regel ü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 es keine APIs zum Zugriff auf die Rohquelle eines externen Stylesheets gibt, muss es möglicherweise asynchron noch einmal abgerufen werden, idealerweise aber nur aus dem Browsercache.

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-Lademechanismus minimiert den Overhead für Nutzer mit neueren Browsern und bietet gleichzeitig ein einfaches Feedback für Nutzer mit älteren Browsern und langsameren Netzwerken.
  • 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.