Im Containerabfrage-Polyfill

Gerald Monaco
Gerald Monaco

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

In diesem Beitrag erfährst du, wie Polyfill funktioniert, welche Herausforderungen er meistert und welche Best Practices du dabei beachten solltest.

Details

Transpilation

Wenn der CSS-Parser in einem Browser auf eine unbekannte At-Regel stößt, z. B. bei der ganz neuen @container-Regel, wird sie einfach verworfen, als ob es sie nie gegeben hätte. Daher ist das Wichtigste, was der Polyfill tun muss, eine @container-Abfrage in etwas umzuwandeln, das nicht verworfen wird.

Der erste Schritt bei der Transpilation besteht darin, die übergeordnete @container-Regel in eine @media-Abfrage zu konvertieren. Dadurch wird in erster Linie sichergestellt, dass die Inhalte gruppiert bleiben. Das ist beispielsweise der Fall, wenn Sie CSSOM APIs verwenden und die CSS-Quelle aufrufen.

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

Vor Containerabfragen hatten Autoren keine Möglichkeit, Regelgruppen willkürlich zu aktivieren oder zu deaktivieren. Für dieses Verhalten müssen auch die Regeln innerhalb einer Containerabfrage transformiert werden. Jeder @container erhält eine eigene eindeutige ID (z. B. 123), die verwendet wird, um jeden Selektor so umzuwandeln, dass er nur angewendet wird, wenn das Element ein cq-XYZ-Attribut hat, das diese ID enthält. Dieses Attribut wird vom 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 das Hinzufügen eines zusätzlichen Attributselektors die Spezifität des Selektors erhöhen. Mit der Pseudoklasse kann die zusätzliche Bedingung angewendet werden, während die ursprüngliche Spezifität erhalten bleibt. Betrachten Sie das folgende Beispiel, um zu sehen, warum dies so wichtig ist:

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

.card {
  color: red;
}

Aufgrund dieses CSS-Codes sollte ein Element mit der .card-Klasse immer color: red haben, da die spätere Regel die vorherige Regel immer mit demselben Selektor und derselben Spezifität überschreiben würde. Das Transpilieren der ersten Regel und das Einbeziehen 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 Polyfill eine sichere und einfache Problemumgehung: Sie können die Spezifität Ihrer Regeln absichtlich erhöhen, indem Sie Ihren @container-Regeln manuell eine Dummy-:not(.container-query-polyfill)-Auswahl 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 eine Reihe von Vorteilen:

  • Der Selektor in der Quell-CSS hat sich geändert, sodass der Unterschied in der Spezifität explizit sichtbar ist. Dies dient auch als Dokumentation, damit du weißt, was betroffen ist, wenn du die Problemumgehung oder den Polyfill nicht mehr unterstützen musst.
  • Die Spezifität der Regeln ist immer gleich, da Polyfill sie nicht ändert.

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

Pseudoelemente

Möglicherweise fragen Sie sich, ob Polyfill ein cq-XYZ-Attribut für ein Element festlegt, um die eindeutige Container-ID 123 aufzunehmen. Wie können dann Pseudoelemente unterstützt werden, auf die keine Attribute festgelegt werden können?

Pseudoelemente sind immer an ein echtes Element im DOM gebunden, das als Ursprungselement bezeichnet wird. Während der Transpilation wird der bedingte Selektor stattdessen 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.

Aber das ist noch nicht alles. Ein Container darf nichts ändern, was nicht in ihm enthalten ist (und ein Container kann nicht für sich selbst sein). Berücksichtigen Sie jedoch, dass das genau der Fall wäre, wenn #foo selbst das abgefragte Containerelement wäre. Das Attribut #foo[cq-XYZ] würde fälschlicherweise geändert und alle #foo-Regeln würden fälschlicherweise angewendet.

Um dies zu korrigieren, verwendet 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 ein Container das erste Attribut (cq-XYZ-A) nie auf sich selbst anwendet, stimmt der erste Selektor nur überein, wenn ein anderer übergeordneter Container die Containerbedingungen erfüllt und angewendet hat.

Relative Containereinheiten

Containerabfragen enthalten außerdem einige neue Einheiten, die Sie in Ihrem CSS-Code verwenden können, z. B. cqw und cqh für jeweils 1% der Breite bzw. Höhe des am nächsten geeigneten übergeordneten Containers. Um diese zu unterstützen, wird die Einheit mithilfe von benutzerdefinierten CSS-Eigenschaften in einen calc(...)-Ausdruck umgewandelt. Polyfill legt die Werte für diese Eigenschaften über Inline-Stile im 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 Inline-Größe bzw. Blockgröße. Diese sind etwas komplizierter, da die Inline- und Blockachsen vom writing-mode des Elements, das die Einheit verwendet, und nicht vom abgefragten Element bestimmt werden. Dazu wendet 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 auch einige neue CSS-Eigenschaften wie container-type und container-name hinzu. Da APIs wie getComputedStyle(...) nicht mit unbekannten oder ungültigen Properties verwendet werden können, werden sie nach dem Parsen auch in benutzerdefinierte CSS-Eigenschaften umgewandelt. Wenn eine Property nicht geparst werden kann, weil sie beispielsweise einen ungültigen oder unbekannten Wert enthält, wird sie einfach vom Browser verarbeitet.

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 umgewandelt, wenn sie entdeckt werden, sodass Polyfill mit anderen CSS-Funktionen wie @supports reibungslos funktionieren kann. Diese Funktion bildet die Grundlage der unten beschriebenen Best Practices für die Verwendung von Polyfill.

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 --cq-XYZ-container-name und --cq-XYZ-container-type übernimmt. Das entspricht definitiv nicht dem Verhalten der nativen Properties. Um dieses Problem zu lösen, fügt Polyfill die folgende Regel vor Nutzerstile ein. Dadurch wird sichergestellt, dass jedes Element die Anfangswerte erhält, sofern diese nicht absichtlich von einer anderen Regel überschrieben wird.

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

Best Practices

Es ist davon auszugehen, dass die meisten Besucher Browser mit integrierter Container-Abfrageunterstützung bereits früher als später ausführen werden. Dennoch ist es wichtig, auch Ihren übrigen Besuchern eine gute Erfahrung zu bieten.

Während des anfänglichen Ladevorgangs muss einiges passieren, bevor das Polyfill-Layout die Seite erstellen kann:

  • Der Polyfill muss geladen und initialisiert werden.
  • Stylesheets müssen geparst und transpiliert werden. Da es keine APIs für den Zugriff auf die Rohquelle eines externen Stylesheets gibt, muss es möglicherweise asynchron noch einmal abgerufen werden, idealerweise aber nur aus dem Browser-Cache.

Wenn der Polyfill diese Probleme nicht sorgfältig respektiert, kann dies zu einem Rückgang der Core Web Vitals führen.

Um die Nutzerfreundlichkeit für Ihre Besucher zu verbessern, wurden bei Polyfill First Input Delay (FID) und Cumulative Layout Shift (CLS) priorisiert, möglicherweise auf Kosten von Largest Contentful Paint (LCP). Konkret garantiert der Polyfill keine Garantie, dass Ihre Containerabfragen vor der ersten Farbe ausgewertet werden. Für eine optimale Nutzererfahrung müssen Sie also dafür sorgen, dass alle Inhalte, deren Größe oder Position durch Containerabfragen beeinträchtigt werden würde, erst ausgeblendet werden, nachdem der Polyfill Ihr CSS geladen und transpiliert hat. Eine Möglichkeit, dies zu erreichen, 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 exakt über Ihrem (verborgenen) Inhalt positioniert ist, um den Besucher über etwas zu informieren. Eine vollständige Demo dieses Ansatzes finden Sie hier.

Dieser Ansatz wird aus mehreren Gründen empfohlen:

  • Ein reines CSS-Ladeprogramm minimiert den Aufwand für Nutzer mit neueren Browsern und bietet Nutzern mit älteren Browsern und langsameren Netzwerken ein einfaches Feedback.
  • Indem Sie die absolute Positionierung des Ladeprogramms mit visibility: hidden kombinieren, vermeiden Sie Layoutverschiebungen.
  • Sobald der Polyfill geladen ist, wird die @supports-Bedingung nicht mehr erfüllt und dein Inhalt wird sichtbar.
  • Bei Browsern mit integrierter Unterstützung für Containerabfragen wird die Bedingung nie erfüllt, sodass die Seite wie erwartet beim ersten Mal angezeigt wird.

Fazit

Wenn Sie Containerabfragen in älteren Browsern verwenden möchten, probieren Sie polyfill aus. Zögern Sie nicht, ein Problem zu melden, wenn Probleme auftreten.

Wir freuen uns darauf, zu sehen und zu sehen, was Sie in Zukunft so alles erleben werden.

Danksagungen

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