Wenn Sass und neue CSS-Funktionen aufeinandertreffen

Avatar of Ana Tudor
Ana Tudor am

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

Kürzlich wurden viele neue coole Funktionen zu CSS hinzugefügt, wie z. B. Benutzerdefinierte Eigenschaften und neue Funktionen. Während diese Dinge unser Leben erheblich erleichtern können, können sie auch auf lustige Weise mit Präprozessoren wie Sass interagieren.

Dies wird also ein Beitrag über die Probleme sein, auf die ich gestoßen bin, wie ich sie umgehe und warum ich Sass auch heute noch für notwendig halte.

Die Fehler

Wenn Sie mit den neuen Funktionen min() und max() experimentiert haben, sind Sie möglicherweise auf eine Fehlermeldung wie diese gestoßen, wenn Sie mit verschiedenen Einheiten gearbeitet haben: „Incompatible units: vh and em.“

Screenshot. Shows the `Incompatible units: 'em' and 'vh'` error when trying to set `width: min(20em, 50vh)`.
Ein Fehler bei der Arbeit mit verschiedenen Einheitentypen in der Funktion min()/ max()

Das liegt daran, dass Sass seine eigene min()-Funktion hat und die CSS-Funktion min() ignoriert. Außerdem kann Sass keine Berechnungen mit zwei Werten durchführen, die Einheiten haben, zwischen denen keine feste Beziehung besteht.

Beispielsweise haben die Einheiten cm und in eine feste Beziehung zueinander, sodass Sass das Ergebnis von min(20in, 50cm) ermitteln kann und keinen Fehler auslöst, wenn wir versuchen, es in unserem Code zu verwenden.

Dasselbe gilt für andere Einheiten. Winkelmaße beispielsweise haben alle eine feste Beziehung zueinander: 1turn, 1rad oder 1grad ergeben immer die gleichen deg-Werte. Dasselbe gilt für 1s, was immer 1000ms ist, 1kHz, was immer 1000Hz ist, 1dppx, was immer 96dpi ist, und 1in, was immer 96px ist. Deshalb kann Sass zwischen ihnen konvertieren und sie in Berechnungen und innerhalb von Funktionen wie seiner eigenen min()-Funktion mischen.

Aber die Dinge brechen, wenn diese Einheiten keine feste Beziehung zueinander haben (wie im früheren Fall mit den Einheiten em und vh).

Und es sind nicht nur unterschiedliche Einheiten. Der Versuch, calc() in min() zu verwenden, führt ebenfalls zu einem Fehler. Wenn ich etwas wie calc(20em + 7px) versuche, lautet der Fehler: „calc(20em + 7px) ist keine Zahl für min.“

Screenshot. Shows the `'calc(20em + 7px)' is not a number for 'min'` error when trying to set `width: min(calc(20em + 7px), 50vh)`.
Ein Fehler bei der Verwendung unterschiedlicher Einheitenwerte mit calc(), verschachtelt in der Funktion min()

Ein weiteres Problem entsteht, wenn wir eine CSS-Variable oder das Ergebnis einer mathematischen CSS-Funktion (wie calc(), min() oder max()) in einem CSS-Filter wie invert() verwenden möchten.

In diesem Fall erhalten wir die Meldung „$color: 'var(--p, 0.85) ist keine Farbe für invert.“

Screenshot. Shows the `$color: 'var(--p, 0.85)' is not a color for 'invert'` error when trying to set `filter: invert(var(--p, .85))`.
var() in filter: invert() Fehler

Dasselbe passiert für grayscale(): „$color: ‚calc(.2 + var(--d, .3))‚ ist keine Farbe für grayscale.“

Screenshot. Shows the `$color: 'calc(.2 + var(--d, .3))' is not a color for 'grayscale'` error when trying to set `filter: grayscale(calc(.2 + var(--d, .3)))`.
calc() in filter: grayscale() Fehler

opacity() verursacht dasselbe Problem: „$color: ‚var(--p, 0.8)‚ ist keine Farbe für opacity.“

Screenshot. Shows the `$color: 'var(--p, 0.8)' is not a color for 'opacity'` error when trying to set `filter: opacity(var(--p, 0.8))`.
var() in filter: opacity() Fehler

Andere filter-Funktionen – darunter sepia(), blur(), drop-shadow(), brightness(), contrast() und hue-rotate() – funktionieren jedoch problemlos mit CSS-Variablen!

Es stellt sich heraus, dass das, was passiert, dem Problem mit min() und max() ähnelt. Sass hat keine integrierten Funktionen für sepia(), blur(), drop-shadow(), brightness(), contrast(), hue-rotate(), aber es hat seine eigenen grayscale()-, invert()- und opacity()-Funktionen, und ihr erster Parameter ist ein $color-Wert. Da sie dieses Argument nicht findet, löst sie einen Fehler aus.

Aus demselben Grund geraten wir auch in Schwierigkeiten, wenn wir eine CSS-Variable verwenden möchten, die mindestens zwei hsl()- oder hsla()-Werte auflistet.

Screenshot. Shows the `wrong number of arguments (2 for 3) for 'hsl'` error when trying to set `color: hsl(9, var(--sl, 95%, 65%))`.
var() in color: hsl() Fehler.

Auf der anderen Seite ist color: hsl(9, var(--sl, 95%, 65%)) gültiges CSS und funktioniert ohne Sass einwandfrei.

Genau dasselbe passiert mit den Funktionen rgb() und rgba().

Screenshot. Shows the `$color: 'var(--rgb, 128, 64, 64)' is not a color for 'rgba'` error when trying to set `color: rgba(var(--rgb, 128, 64, 64), .7)`.
var() in color: rgba() Fehler.

Darüber hinaus, wenn wir Compass importieren und eine CSS-Variable in einem linear-gradient() oder einem radial-gradient() verwenden wollen, erhalten wir einen weiteren Fehler, obwohl die Verwendung von Variablen in conic-gradient() einwandfrei funktioniert (vorausgesetzt, der Browser unterstützt es).

Screenshot. Shows the At least two color stops are required for a linear-gradient error when trying to set background: linear-gradient(var(--c, pink), gold).
var() in background: linear-gradient() Fehler.

Das liegt daran, dass Compass über linear-gradient() und radial-gradient() Funktionen verfügt, aber nie eine conic-gradient() Funktion hinzugefügt hat.

Die Probleme in all diesen Fällen ergeben sich daraus, dass Sass oder Compass gleichnamige Funktionen haben und davon ausgehen, dass dies die Funktionen sind, die wir in unserem Code verwenden wollten.

Verflixt!

Die Lösung

Der Trick dabei ist, sich daran zu erinnern, dass Sass Groß- und Kleinschreibung beachtet, CSS jedoch nicht.

Das bedeutet, wir können Min(20em, 50vh) schreiben und Sass erkennt es nicht als seine eigene min()-Funktion. Es werden keine Fehler ausgelöst und es ist immer noch gültiges CSS, das wie beabsichtigt funktioniert. Ähnlich ermöglicht das Schreiben von HSL()/ HSLA()/ RGB()/ RGBA() oder Invert(), die zuvor erwähnten Probleme zu vermeiden.

Was die Verläufe betrifft, bevorzuge ich normalerweise linear-Gradient() und radial-Gradient(), einfach weil es näher an der SVG-Version liegt, aber die Verwendung mindestens eines Großbuchstabens darin funktioniert einwandfrei.

Aber warum?

Fast jedes Mal, wenn ich etwas Sass-bezogenes twitte, werde ich belehrt, dass es jetzt nicht mehr verwendet werden sollte, da wir CSS-Variablen haben. Ich dachte, ich greife das auf und erkläre, warum ich dem widerspreche.

Erstens, obwohl ich CSS-Variablen immens nützlich finde und sie in den letzten drei Jahren für fast alles verwendet habe, sollte man bedenken, dass sie mit Leistungskosten verbunden sind und dass die Fehlersuche, wo etwas in einem Labyrinth von calc()-Berechnungen schiefgelaufen ist, mit unseren aktuellen DevTools mühsam sein kann. Ich versuche, sie nicht zu überbeanspruchen, um nicht in ein Territorium zu geraten, in dem die Nachteile ihrer Verwendung die Vorteile überwiegen.

Screenshot. Shows how `calc()` expressions are presented in DevTools.
Nicht gerade einfach zu verstehen, was das Ergebnis dieser calc()-Ausdrücke ist.

Im Allgemeinen, wenn es sich wie eine Konstante verhält, sich von Element zu Element oder von Zustand zu Zustand nicht ändert (in welchem ​​Fall benutzerdefinierte Eigenschaften definitiv der richtige Weg sind) oder die Menge an kompiliertem CSS reduziert (löst das Wiederholungsproblem, das durch Präfixe entsteht), dann werde ich eine Sass-Variable verwenden.

Zweitens waren Variablen schon immer ein ziemlich kleiner Teil des Grundes, warum ich Sass verwende. Als ich Ende 2012 anfing, Sass zu verwenden, war es hauptsächlich zum Schleifen gedacht, eine Funktion, die wir in CSS immer noch nicht haben. Obwohl ich einige dieser Schleifen in einen HTML-Präprozessor verschoben habe (weil es den generierten Code reduziert und vermeidet, sowohl das HTML als auch das CSS später ändern zu müssen), verwende ich immer noch Sass-Schleifen in vielen Fällen, z. B. beim Generieren von Wertelisten, Stopplisten innerhalb von Gradientenfunktionen, Listen von Punkten innerhalb einer Polygonfunktion, Listen von Transformationen usw.

Hier ist ein Beispiel. Ich habe früher n HTML-Elemente mit einem Präprozessor generiert. Die Wahl des Präprozessors spielt weniger eine Rolle, aber ich werde hier Pug verwenden.

- let n = 12;

while n--
  .item

Dann würde ich die Variable $n in Sass festlegen (und sie müsste mit der in HTML übereinstimmen) und bis dahin schleifen, um die Transformationen zu generieren, die jedes Element positionieren würden

$n: 12;
$ba: 360deg/$n;
$d: 2em;

.item {
  position: absolute;
  top: 50%; left: 50%;
  margin: -.5*$d;
  width: $d; height: $d;
  /* prettifying styles */

  @for $i from 0 to $n {
    &:nth-child(#{$i + 1}) {
      transform: rotate($i*$ba) translate(2*$d) rotate(-$i*$ba);
			
      &::before { content: '#{$i}' }
    }
  }
}

Das bedeutete jedoch, dass ich sowohl Pug als auch Sass ändern müsste, wenn sich die Anzahl der Elemente ändert, was den generierten Code sehr repetitiv machte.

Screenshot. Shows the generated CSS, really verbose, almost completely identical transform declaration repeated for each item.
CSS, generiert durch den obigen Code

Ich habe inzwischen dazu übergegangen, Pug die Indizes als benutzerdefinierte Eigenschaften generieren zu lassen und diese dann in der transform-Deklaration zu verwenden.

- let n = 12;

body(style=`--n: ${n}`)
  - for(let i = 0; i < n; i++)
    .item(style=`--i: ${i}`)
$d: 2em;

.item {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -.5*$d;
  width: $d;
  height: $d;
  /* prettifying styles */
  --az: calc(var(--i)*1turn/var(--n));
  transform: rotate(var(--az)) translate(2*$d) rotate(calc(-1*var(--az)));
  counter-reset: i var(--i);
	
  &::before { content: counter(i) }
}

Dies reduziert den generierten Code erheblich.

Screenshot. Shows the generated CSS, much more compact, no having almost the exact same declaration set on every element separately.
CSS, generiert durch den obigen Code

Das Schleifen in Sass ist jedoch immer noch notwendig, wenn ich etwas wie einen Regenbogen generieren möchte.

@function get-rainbow($n: 12, $sat: 90%, $lum: 65%) {
  $unit: 360/$n;
  $s-list: ();
	
  @for $i from 0 through $n {
    $s-list: $s-list, hsl($i*$unit, $sat, $lum)
  }
	
  @return $s-list
}

html { background: linear-gradient(90deg, get-rainbow()) }

Sicher, ich könnte es als Listenvariable von Pug generieren, aber das würde die dynamische Natur von CSS-Variablen nicht nutzen und den an den Browser gelieferten Code nicht reduzieren, sodass kein Vorteil daraus entsteht.

Ein weiterer wichtiger Teil meiner Sass- (und Compass-) Nutzung hängt mit integrierten mathematischen Funktionen (wie trigonometrischen Funktionen) zusammen, die jetzt Teil der CSS-Spezifikation sind, aber noch in keinem Browser implementiert sind. Sass bietet diese Funktionen ebenfalls nicht, aber Compass tut dies, und deshalb muss ich oft Compass verwenden.

Und klar, ich könnte meine eigenen solchen Funktionen in Sass schreiben. Das habe ich am Anfang gemacht, bevor Compass inverse trigonometrische Funktionen unterstützte. Ich brauchte sie wirklich, also schrieb ich meine eigenen basierend auf der Taylor-Reihe. Aber Compass bietet diese Art von Funktionen heutzutage an und sie sind besser und leistungsfähiger als meine.

Mathematische Funktionen sind für mich äußerst wichtig, da ich ein Techniker und kein Künstler bin. Die Werte in meinem CSS ergeben sich normalerweise aus mathematischen Berechnungen. Es sind keine magischen Zahlen oder etwas, das rein für ästhetische Zwecke verwendet wird. Ein Beispiel ist die Generierung von Listen von Clip-Pfadpunkten, die reguläre oder quasi-reguläre Polygone erstellen. Denken Sie an den Fall, in dem wir Dinge wie nicht-rechteckige Avatare oder Aufkleber erstellen möchten.

Betrachten wir ein regelmäßiges Polygon mit Scheitelpunkten auf einem Kreis mit einem Radius von 50% des quadratischen Elements, von dem wir ausgehen. Das Verschieben des Schiebereglers in der folgenden Demo ermöglicht es uns, zu sehen, wo die Punkte für verschiedene Anzahlen von Scheitelpunkten platziert sind

Wenn wir das in Sass-Code umwandeln, haben wir

@mixin reg-poly($n: 3) {
  $ba: 360deg/$n; // base angle
  $p: (); // point coords list, initially empty
	
  @for $i from 0 to $n {
    $ca: $i*$ba; // current angle
    $x: 50%*(1 + cos($ca)); // x coord of current point
    $y: 50%*(1 + sin($ca)); // y coord of current point
    $p: $p, $x $y // add current point coords to point coords list
  }
	
  clip-path: polygon($p) // set clip-path to list of points
}

Beachten Sie, dass wir hier auch Schleifen und Dinge wie Bedingungen und Modulo verwenden, die bei der Verwendung von CSS ohne Sass sehr mühsam sind.

Eine etwas weiter entwickelte Version davon könnte das Drehen des Polygons beinhalten, indem derselbe Offset-Winkel ($oa) zum Winkel jedes Scheitelpunkts addiert wird. Dies ist in der folgenden Demo zu sehen. Dieses Beispiel fügt ein Stern-Mixin hinzu, das auf ähnliche Weise funktioniert, außer dass wir immer eine gerade Anzahl von Scheitelpunkten haben und jeder ungeradzahlige Scheitelpunkt auf einem Kreis mit einem kleineren Radius liegt ($f*50%, wobei $f sub-unitary ist)

Wir können auch fette Sterne wie diese haben

Oder Aufkleber mit interessanten border-Mustern. In dieser speziellen Demo wird jeder Aufkleber mit einem einzigen HTML-Element erstellt und das border-Muster mit clip-path, Schleifen und Mathematik in Sass erstellt. Ziemlich viel davon, tatsächlich.

Ein weiteres Beispiel sind diese Kartenhintergründe, bei denen Schleifen, die Modulus-Operation und Exponentialfunktionen zusammenarbeiten, um die dithering-Pixel-Hintergrundebenen zu generieren

Diese Demo setzt zufällig auch stark auf CSS-Variablen.

Dann gibt es die Verwendung von Mixins, um zu vermeiden, dass dieselben Deklarationen immer wieder geschrieben werden, wenn Dinge wie Bereichseingaben gestylt werden. Verschiedene Browser verwenden unterschiedliche Pseudoelemente, um die Komponenten eines solchen Steuerelements zu stylen, daher müssen wir für jede Komponente die Stile festlegen, die ihr Aussehen bei mehreren Pseudoelementen steuern.

Leider, so verlockend es auch sein mag, dies in unser CSS einzufügen

input::-webkit-slider-runnable-track, 
input::-moz-range-track, 
input::-ms-track { /* common styles */ }

… können wir das nicht tun, weil es nicht funktioniert! Der gesamte Regelblock wird gelöscht, wenn auch nur einer der Selektoren nicht erkannt wird. Und da kein Browser alle drei der oben genannten erkennt, werden die Stile in keinem Browser angewendet.

Wir brauchen so etwas, wenn wir wollen, dass unsere Stile angewendet werden

input::-webkit-slider-runnable-track { /* common styles */ }
input::-moz-range-track { /* common styles */ }
input::-ms-track { /* common styles */ }

Aber das kann viele identische Stile bedeuten, die dreimal wiederholt werden. Und wenn wir beispielsweise den background des Tracks ändern wollen, müssen wir ihn in den ::-webkit-slider-runnable-track-Stilen, in den ::-moz-range-track-Stilen und in den ::-ms-track-Stilen ändern.

Die einzig vernünftige Lösung, die wir haben, ist die Verwendung eines Mixins. Die Stile werden im kompilierten Code wiederholt, weil sie dort wiederholt werden müssen, aber wir müssen nicht mehr dasselbe dreimal schreiben.

@mixin track() { /* common styles */ }

input {
  &::-webkit-slider-runnable-track { @include track }
  &::-moz-range-track { @include track }
  &::-ms-track { @include track }
}

Fazit: Ja, Sass ist im Jahr 2020 immer noch sehr notwendig.