Meistens schreibe ich reines CSS. Dank CSS-Variablen und Nesting habe ich immer seltener Gründe, auf Sass oder andere Präprozessoren zurückzugreifen. Die Momente, in denen ich doch zu Sass greife, sind meistens dann, wenn ich ein @mixin benötige, um eine Liste von Elementen zu durchlaufen oder um gängige Stile DRY (Don't Repeat Yourself) zu halten.
Das könnte sich in nicht allzu ferner Zukunft ändern, da Ende Juni ein neuer Entwurf für das CSS Functions and Mixins Module veröffentlicht wurde, nachdem die CSSWG bereits im Februar beschlossen hatte, den Vorschlag anzunehmen.
Beachten Sie den Namen des Moduls: Functions and Mixins. Es gibt einen Unterschied zwischen den beiden.
Das alles ist noch neu und momentan noch sehr unausgegoren, mit vielen TODO-Anmerkungen im Entwurf und Punkten, die in zukünftigen Entwürfen berücksichtigt werden müssen. Die Draft-Spezifikation hat noch nicht einmal eine Definition für Mixins. Es wird wahrscheinlich noch einige Zeit dauern, bis wir etwas Reales zum Arbeiten und Experimentieren bekommen, aber ich mag es, mich schon in der frühen Phase mit solchen Dingen auseinanderzusetzen, auch wenn ich weiß, dass sich vieles noch ändern wird.
Zusätzlich zum frühen Entwurf der Spezifikation hat Miriam Suzanne einen ausführlichen Explainer veröffentlicht, der hilft, einige Informationslücken zu schließen. Miriam ist Editorin der Spezifikation, daher finde ich alles, was sie darüber schreibt, als nützlichen Kontext.
Es gibt viel zu lesen! Hier sind meine wichtigsten Erkenntnisse…
Eigene Funktionen sind fortgeschrittene Custom Properties
Wir sprechen hier nicht von den zweckgebundenen, integrierten Funktionen, die wir in den letzten Jahren schätzen gelernt haben – wie z. B. calc(), min(), max() usw. Stattdessen sprechen wir von benutzerdefinierten Funktionen (custom functions), die mit einer @function-At-Regel definiert werden und Logik zur Rückgabe eines erwarteten Wertes enthalten.
Das macht benutzerdefinierte Funktionen einer Custom Property (CSS-Variable) sehr ähnlich. Eine Custom Property ist lediglich ein Platzhalter für einen erwarteten Wert, den wir normalerweise im Voraus definieren.
:root {
--primary-color: hsl(25 100% 50%);
}
Benutzerdefinierte Funktionen sehen ziemlich ähnlich aus, werden aber mit @function definiert und nehmen Parameter entgegen. Dies ist die Syntax, die derzeit im Entwurf der Spezifikation steht.
@function <function-name> [( <parameter-list> )]? {
<function-rules>
result: <result>;
}
Das result ist das, worauf der endgültige Wert der benutzerdefinierten Funktion hinausläuft. Es ist momentan noch etwas verwirrend für mich, aber ich verstehe es so, dass eine benutzerdefinierte Funktion eine benutzerdefinierte Property zurückgibt. Hier ist ein Beispiel direkt aus dem Spezifikationsentwurf (leicht modifiziert), das die Fläche eines Kreises berechnet.
@function --circle-area(--r) {
--r2: var(--r) * var(--r);
result: calc(pi * var(--r2));
}
Der Aufruf der Funktion ist so ähnlich wie das Deklarieren einer Custom Property, nur ohne var() und mit Argumenten für die definierten Parameter.
.element {
inline-size: --circle-area(--r, 1.5rem); /* = ~7.065rem */
}
Es scheint, als könnten wir dasselbe wie mit einer Custom Property mit aktuellen CSS-Funktionen erreichen.
:root {
--r: 1rem;
--r2: var(--r) * var(--r);
--circle-area: calc(pi * var(--r2));
}
.element {
inline-size: var(--circle-area, 1.5rem);
}
Davon abgesehen sind die Gründe, warum wir eine benutzerdefinierte Funktion einer Custom Property vorziehen würden, (1) dass sie auf einen Schlag einen von mehreren Werten zurückgeben können und (2) dass sie bedingte Regeln wie @supports und @media unterstützen, um zu bestimmen, welcher Wert zurückgegeben werden soll. Schauen Sie sich Miriams Beispiel für eine benutzerdefinierte Funktion an, die basierend auf der Inline-Größe des Viewports einen von mehreren Werten zurückgibt.
/* Function name */
@function --sizes(
/* Array of possible values */
--s type(length),
--m type(length),
--l type(length),
/* The returned value with a default */
) returns type(length) {
--min: 16px;
/* Conditional rules */
@media (inline-size < 20em) {
result: max(var(--min), var(--s, 1em));
}
@media (20em < inline-size < 50em) {
result: max(var(--min), var(--m, 1em + 0.5vw));
}
@media (50em < inline-size) {
result: max(var(--min), var(--l, 1.2em + 1vw));
}
}
Miriam erklärt weiter, wie eine komma-separierte Liste von Parametern wie diese zusätzliche Arbeit der CSSWG erfordert, da sie fälschlicherweise als zusammengesetzter Selektor interpretiert werden könnte.
Mixins helfen dabei, DRY-Prinzipien und wiederverwendbare Style-Blöcke zu wahren
Mixins kommen mir vertrauter vor als benutzerdefinierte Funktionen. Jahrelanges Schreiben von Sass-Mixins hinterlässt Spuren, und tatsächlich ist das vielleicht der Hauptgrund, warum ich immer noch ab und zu zu Sass greife.
Mixins sehen den neuen benutzerdefinierten Funktionen ähnlich. Statt @function arbeiten wir mit @mixin, was genau so funktioniert wie in Sass.
/* Custom function */
@function <function-name> [( <parameter-list> )]? {
<function-rules>
result: <result>;
}
/* CSS/Sass mixin */
@mixin <mixin-name> [( <parameter-list> )]? {
<mixin-rules>
}
Benutzerdefinierte Funktionen und Mixins sind sich also recht ähnlich, aber sie unterscheiden sich definitiv.
- Funktionen werden mit
@functiondefiniert; Mixins werden mit@mixindefiniert, aber beide werden mit einem Identifikator mit Bindestrichen benannt (z. B.--name). - Funktionen resultieren in einem Wert (
result); Mixins resultieren in Style-Regeln.
Das macht Mixins ideal für das Abstrahieren von Stilen, die man als Utility-Klassen verwenden könnte, beispielsweise eine Klasse für versteckten Text, der nur von Screenreadern gelesen wird.
.sr-text {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
In echter Utility-Manier können wir diese Klasse auf Elemente im HTML verteilen, um den Text zu verstecken.
<a class="sr-text">Skip to main content</a>
Super praktisch! Aber wie jeder Tailwind-Hasser sagen wird, kann dies zu hässlichem Markup führen, das schwer zu interpretieren ist, wenn wir uns auf *viele* Utility-Klassen verlassen. Screenreader-Text ist da nicht so gefährdet, aber ein kurzes Beispiel aus den Tailwind-Docs sollte diesen Punkt verdeutlichen.
<div class="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg">
Es ist eigentlich eine Frage des Geschmacks. Aber zurück zu den Mixins! Der Punkt ist, dass wir Utility-Klassen fast wie kleine CSS-Snippets verwenden können, um andere Style-Regeln aufzubauen und eine klarere Trennung zwischen Markup und Stilen beizubehalten. Wenn wir die gleichen .sr-text Stile von vorhin nehmen und sie „mixin-isieren“ (ja, das Wort habe ich gerade erfunden):
@mixin --sr-text {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
Anstatt ins HTML zu springen, um die Stile anzuwenden, können wir sie in andere CSS-Style-Regeln mit einer neuen @apply At-Regel einbetten.
header a:first-child {
@apply --sr-text;
/* Results in: */
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
Vielleicht ist ein besseres Beispiel etwas, das jedes Projekt zu brauchen scheint: etwas zentrieren!
@mixin --center-me {
display: grid;
place-items: center;
}
Dies kann nun Teil eines größeren Regelsatzes sein.
header {
@apply --center-me;
/*
display: grid;
place-items: center;
*/
background-color: --c-blue-50;
color: --c-white;
/* etc. */
}
Das unterscheidet sich von Sass, das @include verwendet, um das Mixin aufzurufen, anstatt @apply. Wir können sogar größere Blöcke von Stilen zurückgeben, wie z. B. Stile für die ::before und ::after Pseudoelemente eines Elements.
@mixin --center-me {
display: grid;
place-items: center;
position: relative;
&::after {
background-color: hsl(25 100% 50% / .25);
content: "";
height: 100%;
position: absolute;
width: 100%;
}
}
Und natürlich haben wir gesehen, dass Mixins genau wie benutzerdefinierte Funktionen Argumentparameter akzeptieren. Man könnte Argumente verwenden, wenn man die Stile für Variationen lockern möchte, wie zum Beispiel die Definition konsistenter Gradienten mit unterschiedlichen Farben.
@mixin --gradient-linear(--color-1, --color-2, --angle) {
/* etc. */
}
Wir können die Syntax für jeden Parameter als eine Form der Typprüfung angeben.
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
/* etc. */
}
Wir können diese Variablen weiter abstrahieren und Standardwerte für sie festlegen.
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
--from: var(--color-1, orangered);
--to: var(--from-color, goldenrod);
--angle: var(--at-angle, to bottom right);
/* etc. */
}
…dann schreiben wir die Style-Regeln des Mixins mit den Parametern als Variablen-Platzhalter.
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
--from: var(--color-1, orangered);
--to: var(--from-color, goldenrod);
--angle: var(--at-angle, to bottom right);
background: linear-gradient(var(--angle), var(--from), var(--to));
}
Fügen Sie dort bedingte Logik hinzu, wenn Sie möchten.
@mixin --gradient-linear(
--color-1 type(color),
--color-2 type(color),
--angle type(angle),
) {
--from: var(--color-1, orangered);
--to: var(--from-color, goldenrod);
--angle: var(--at-angle, to bottom right);
background: linear-gradient(var(--angle), var(--from), var(--to));
@media (prefers-contrast: more) {
background: color-mix(var(--from), black);
color: white;
}
}
Dies ist alles bereit, um das Mixin mit @apply in beliebigen Regelsätzen anzuwenden.
header {
@apply --gradient-linear;
/* etc. */
}
.some-class {
@apply --gradient-linear;
/* etc. */
}
…und sie mit anderen Mixins zu kombinieren.
header {
@apply --gradient-linear;
@apply --center-me;
/* etc. */
}
Das ist alles sehr oberflächlich. Miriam geht auf Nuancen ein wie:
- Anwenden von Mixins auf Root-Ebene (d. h. nicht in einem Selektor).
- Arbeiten mit Container Queries mit der Einschränkung, globale Custom Properties auf einem anderen Element als dem abgefragten setzen zu müssen.
- Die Möglichkeit, Mixin-Parameter bedingt mit so etwas wie
@when/@elseim Mixin zu setzen. (Was mich fragen lässt, was mit der neu vorgeschlagenenif()-Funktion ist und ob sie anstelle von@whenverwendet werden würde.) - Warum wir eine Grenze ziehen könnten, wenn es darum geht, Schleifen auf die gleiche Weise wie Sass zu unterstützen. (CSS ist eine deklarative Sprache und Schleifen sind imperative Abläufe.)
- Scoping von Mixins (
@layer?scope? Etwas anderes?)
Miriam hat eine exzellente Übersicht über die offenen Fragen und Diskussionen rund um Mixins.
Das war’s erst einmal… zumindest vorerst.
Puh, das ist eine Menge Stoff! Jedes Mal, wenn ich tief in Entwürfen für CSS-Spezifikationen stecke, muss ich mich daran erinnern, dass sich der Staub erst noch legen muss. Die Autoren und Editoren der Spezifikation ringen mit vielen der gleichen Fragen, die wir haben – und mehr! Es ist also nicht so, dass ein flüchtiges Lesen der Entwürfe jemanden zum Experten macht. Und das ist noch bevor wir zu dem Punkt kommen, dass sich Dinge ändern können und wahrscheinlich auch werden, bis sie zu einem empfohlenen Feature für Browser zur Implementierung werden.
Es wird ein interessantes Feld sein, das man im Auge behalten sollte, was man mit den folgenden Ressourcen tun kann:
- Vorschlag: Custom CSS Functions & Mixins (GitHub Issue #9350)
- CSS Mixins & Functions Explainer (Miriam Suzanne)
- Layered Toggles: Optionale CSS Mixins (Roman Komarov)
- Alle GitHub-Issues mit dem Tag
css-mixins