Tiefer eintauchen in Container-Style-Abfragen

Avatar of Geoff Graham
Geoff Graham am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Vor einiger Zeit habe ich meine ersten Gedanken zu Container-Style-Abfragen aufgeschrieben. Es steckt noch in den Kinderschuhen. Sie sind bereits im CSS Containment Module Level 3 Specification (derzeit im Status "Editor's Draft") definiert, aber es gibt noch einige offene Diskussionen.

Die Grundidee ist, dass wir einen Container definieren und dann stilabhängig bedingt Stile auf seine Nachkommen anwenden können, basierend auf seiner berechneten Formatierung.

@container <name>? <conditions> {
  /* conditional styles */
}

Das beste Beispiel, das ich bisher gesehen habe, ist das Entfernen von Kursivschrift aus Elementen wie <code>&lt;em&gt;</code>, <code>&lt;i&gt;</code> und <code>&lt;q&gt;</code>, wenn sie in einem Kontext verwendet werden, in dem der Inhalt bereits kursiv ist.

em, i, q {
  font-style: italic; /* default UA behavior */
}

/* When the container's font-style is italic, remove italics from these elements. */
@container style(font-style: italic) {
  em, i, q {
    font-style: normal;
  }
}

Das ist die allgemeine Idee. Aber falls Sie es noch nicht wussten, Miriam Suzanne, eine der Herausgeberinnen der Spezifikation, führt eine fortlaufende und gründliche Sammlung persönlicher Notizen zu Container-Style-Abfragen, die öffentlich zugänglich ist. Sie wurde neulich aktualisiert und ich habe einige Zeit damit verbracht, mehr nuancierte Aspekte von Style-Abfragen zu verstehen. Das sind zwar inoffizielle Dinge, aber ich dachte, ich schreibe ein paar Punkte auf, die mir aufgefallen sind. Wer weiß? Vielleicht können wir uns ja in Zukunft darauf freuen!

Jedes Element ist ein Style-Container

Wir müssen nicht einmal explizit einen container-name oder container-type zuweisen, um einen Style-Container zu definieren, da alles standardmäßig ein Style-Container ist.

Sie sehen also das obige Beispiel, das Kursivschrift entfernt? Beachten Sie, dass kein Container identifiziert wird. Es springt direkt zur Abfrage mit der style()-Funktion. Welcher Container wird also abgefragt? Es wird der direkte Elternteil der Elemente sein, die die angewendeten Stile erhalten. Und wenn nicht, dann ist es der nächstgelegene relative Container, der Vorrang hat.

Das gefällt mir. Es ist sehr CSS-artig, dass die Abfrage nach oben nach einer Übereinstimmung sucht und dann weiter nach oben blubbert, bis sie eine übereinstimmende Bedingung findet.

Es war schwer für mein kleines Gehirn zu verstehen, warum wir mit einem impliziten Container basierend auf Stilen auskommen, aber nicht so sehr, wenn wir mit Dimensionsabfragen wie size und inline-size arbeiten. Miriam erklärt es gut.

Dimensionsabfragen erfordern eine CSS-containment für Größe, Layout und Stil des Containers, um Layout-Schleifen zu verhindern. Containment ist eine invasive Sache, die man allgemein anwendet, daher war es wichtig, dass Autoren sorgfältige Kontrolle darüber haben, welche Elemente Größen-Container sind (oder nicht sind).

Stilbasierte Abfragen haben nicht dieselbe Einschränkung. Es gibt bereits keinen Weg in CSS, wie Nachkommenstile die berechneten Stile eines Vorfahren beeinflussen können. Daher ist kein Containment erforderlich, und es gibt keine invasiven oder unerwarteten Nebeneffekte bei der Einrichtung eines Elements als Style-Query-Container.

(Hervorhebung von mir)

Es läuft alles auf die Konsequenzen hinaus – von denen es keine gibt, wenn alles sofort als Style-Query-Container betrachtet wird.

  • Wenn ein Container gefunden wird: Bedingungen werden gegen diesen Container aufgelöst.
  • Wenn mehrere Container übereinstimmen: Der nächstgelegene relative Container hat Vorrang.
  • Wenn keine Übereinstimmung gefunden wird: unknown wird zurückgegeben.

Das ist derselbe "nachsichtige" Geist wie beim Rest von CSS.

Ein Container kann sowohl Dimensions- als auch Style-Abfragen unterstützen

Nehmen wir an, wir möchten eine Style-Abfrage ohne expliziten container-name definieren.

@container style(font-style: italic) {
  em {
    font-style: normal;
  }
}

Das funktioniert, weil *alle Elemente Style-Container* sind, unabhängig vom container-type. Das erlaubt es uns, Stile implizit abzufragen und auf die nächste passende Übereinstimmung zu vertrauen. Und das ist absolut in Ordnung, da es, wie gesagt, keine negativen Nebeneffekte gibt, wenn Style-Container eingerichtet werden.

Wir müssen einen expliziten container-type für Dimensionsabfragen verwenden, aber nicht unbedingt für Style-Abfragen, da jedes Element eine Style-Abfrage ist. Das bedeutet auch, dass dieser Container sowohl eine Style- *als auch* eine Dimensionsabfrage ist.

.card-container {
  container: card / inline-size; /* implictly a style query container as well */
}

Einen Container von der Abfrage ausschließen

Vielleicht möchten wir nicht, dass ein Container am Abfrageprozess teilnimmt. Hier könnte es möglich sein, container-type: none auf ein Element zu setzen.

.some-element {
  container-type: none;
}

Explizite Style-Query-Container bieten mehr Kontrolle darüber, was abgefragt wird

Wenn wir beispielsweise eine Style-Abfrage für padding schreiben würden, gibt es keine zuverlässige Möglichkeit, den besten passenden Container zu ermitteln, unabhängig davon, ob wir mit einem explizit benannten Container oder dem nächstgelegenen direkten Elternteil arbeiten. Das liegt daran, dass padding keine vererbbare Eigenschaft ist.

In diesen Fällen sollten wir also container-name verwenden, um dem Browser explizit mitzuteilen, aus welchen Containern er ziehen kann. Wir können einem Container sogar mehrere explizite Namen geben, damit er mehr Bedingungen erfüllt.

.card {
  container-name: card layout theme;
}

Oh, und container-name akzeptiert beliebig viele optionale und *wiederverwendbare* Namen für einen Container! Das bietet noch mehr Flexibilität, wenn es darum geht, dem Browser bei der Suche nach Übereinstimmungen zu helfen.

.theme {
  container-name: theme;
}
.grid {
  container-name: layout;
}
.card {
  container-name: card layout theme;
}

Ich frage mich ein wenig, ob das nicht auch als "Fallback" betrachtet werden könnte, falls ein Container übersehen wird.

Style-Abfragen können kombiniert werden

Die Operatoren or und and erlauben es uns, Abfragen zu kombinieren, um Dinge DRY (Don't Repeat Yourself) zu halten.

@container bubble style(--arrow-position: start start) or style(--arrow-position: end start) {
  .bubble::after {
    border-block-end-color: inherit;
    inset-block-end: 100%;
  }
}

/* is the same as... */
@container bubble style(--arrow-position: start start) {
  /* etc. */
}
@container bubble style(--arrow-position: end start) {
  /* etc. */
}

Stile umschalten

Es gibt eine kleine Überschneidung zwischen Container-Style-Abfragen und der Arbeit, die an der Definition einer toggle()-Funktion geleistet wird. Zum Beispiel können wir durch zwei font-style-Werte iterieren, sagen wir italic und normal.

em, i, q {
  font-style: italic;
}

@container style(font-style: italic) {
  em, i, q {
    font-style: normal;
  }
}

Cool. Aber der Vorschlag für CSS Toggles deutet darauf hin, dass die toggle()-Funktion ein einfacherer Ansatz wäre.

em, i, q {
  font-style: toggle(italic, normal);
}

Aber alles, was über diesen binären Anwendungsfall hinausgeht, ist, wo toggle() weniger geeignet ist. Style-Abfragen sind jedoch gut zu gebrauchen. Miriam identifiziert drei Fälle, in denen Style-Abfragen besser geeignet sind als toggle().

/* When font-style is italic, apply background color. */
/* Toggles can only handle one property at a time. */
@container style(font-style: italic) {
  em, i, q {
    background: lightpink;
  }
}

/* When font-style is italic and --color-mode equals light */
/* Toggles can only evaluate one condition at a time */
@container style((font-style: italic) and (--color-mode: light)) {
  em, i, q {
    background: lightpink;
  }
}

/* Apply the same query condition to multiple properties */
/* Toggles have to set each one individually as separate toggles */
@container style(font-style: italic) {
  em, i, q {
    /* clipped gradient text */
    background: var(--feature-gradient);
    background-clip: text;
    box-decoration-break: clone;
    color: transparent;
    text-shadow: none;
  }
}

Style-Abfragen lösen den „Custom Property Toggle Hack“

Beachten Sie, dass Style-Abfragen eine formelle Lösung für den „CSS Custom Property Toggle Trick“ sind. Dort setzen wir eine leere benutzerdefinierte Eigenschaft (--foo: ;) und verwenden die durch Kommas getrennte Fallback-Methode, um Eigenschaften ein- und auszuschalten, wenn die benutzerdefinierte Eigenschaft auf einen echten Wert gesetzt wird.

button {
  --is-raised: ; /* off by default */
  
  border: 1px solid var(--is-raised, rgb(0 0 0 / 0.1));
  box-shadow: var(
    --is-raised,
    0 1px hsl(0 0% 100% / 0.8) inset,
    0 0.1em 0.1em -0.1em rgb(0 0 0 / 0.2)
  );
  text-shadow: var(--is-raised, 0 -1px 1px rgb(0 0 0 / 0.3));
}

button:active {
  box-shadow: var(--is-raised, 0 1px 0.2em black inset);
}

#foo {
  --is-raised: initial; /* turned on, all fallbacks take effect. */
}

Das ist super cool, aber auch viel Arbeit, die Style-Container-Abfragen trivial machen.

Style-Abfragen und CSS-generierter Inhalt

Für generierten Inhalt, der durch die content-Eigenschaft von ::before- und ::after-Pseudo-Elementen erzeugt wird, ist der übereinstimmende Container das Element, auf dem der Inhalt generiert wird.

.bubble {
  --arrow-position: end end;
  container: bubble;
  border: medium solid green;
  position: relative;
}

.bubble::after {
  content: "";
  border: 1em solid transparent;
  position: absolute;
}

@container bubble style(--arrow-position: end end) {
  .bubble::after {
    border-block-start-color: inherit;
    inset-block-start: 100%;
    inset-inline-end: 1em;
  }
}

Style-Abfragen und Webkomponenten

Wir können eine Webkomponente als Container definieren und sie per Style abfragen. Zuerst haben wir das <code>&lt;template&gt;</code> der Komponente.

<template id="media-host">
  <article>
    <div part="img">
      <slot name="img">…</slot>
    </div>
    <div part="content">
      <slot name="title">…</slot>
      <slot name="content">…</slot>
    </div>
  </article>
</template>

Dann verwenden wir das Pseudo-Element :host als Container, um einen container-name, einen container-type und einige übergeordnete Attribute darauf zu setzen.

:host {
  container: media-host / inline-size;
  --media-location: before;
  --media-style: square;
  --theme: light;
}

Elemente innerhalb des &lt;media-host&gt; können die Parameter des &lt;media-host&gt;-Elements abfragen.

@container media-host style(--media-style: round) {
  [part='img'] {
    border-radius: 100%;
  }
}

Was kommt als Nächstes?

Auch hier basieren alle von mir notierten Dinge auf Miriams Notizen, und diese Notizen sind kein Ersatz für die offizielle Spezifikation. Aber sie sind ein Hinweis darauf, was diskutiert wird und wohin die Dinge in Zukunft gehen könnten. Ich schätze, Miriam hat eine Handvoll offener Diskussionen verlinkt, die noch stattfinden, und die wir verfolgen können, um auf dem Laufenden zu bleiben.