DRY-Umschaltung mit CSS-Variablen: Der Unterschied einer einzigen Deklaration

Avatar of Ana Tudor
Ana Tudor am

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

Dies ist der erste Teil einer zweiteiligen Serie, die sich damit beschäftigt, wie CSS-Variablen verwendet werden können, um den Code für komplexe Layouts und Interaktionen einfacher zu schreiben und viel leichter zu pflegen. Diese erste Folge stellt verschiedene Anwendungsfälle vor, bei denen diese Technik zum Einsatz kommt. Der zweite Teil behandelt die Verwendung von Fallbacks und ungültigen Werten, um die Technik auf nicht-numerische Werte zu erweitern.

Was wäre, wenn ich Ihnen sagen würde, dass eine einzige CSS-Deklaration den Unterschied im folgenden Bild zwischen dem Breitbildfall (links) und dem zweiten (rechts) ausmacht? Und was wäre, wenn ich Ihnen sagen würde, dass eine einzige CSS-Deklaration den Unterschied zwischen den geraden und ungeraden Elementen im Breitbildfall ausmacht?

On the left, a screenshot of the wide screen scenario. Each item is limited in width and its components are arranged on a 2D 2x2 grid, with the first level heading occupying an entire column, either the one on the right (for odd items) or the one on the left (for even items). The second level heading and the actual text occupy the other column. The shape of the first level heading also varies depending on the parity — it has the top left and the bottom right corners rounded for the odd items and the other two corners rounded for the even items. On the right, a screenshot of the narrower scenario. Each item spans the full viewport width and its components are placed vertically, one under another — first level heading, second level heading below and, finally, the actual text.
Screenshot-Collage.

Oder dass eine einzige CSS-Deklaration den Unterschied zwischen den eingeklappten und ausgeklappten Fällen unten ausmacht?

Animated gif. Shows a green button with a magnifier icon. Clicking this button makes it slide right and its background to turn red while a text search field slides out of it to the left and the magnifier morphs into a close (crossmark) icon.
Erweiterbare Suche.

Wie ist das überhaupt möglich?

Nun, wie Sie sich vom Titel bereits gedacht haben, liegt es alles in der Macht der CSS-Variablen.

Es gibt bereits viele Artikel darüber, was CSS-Variablen sind und wie man damit anfängt, daher werden wir uns hier nicht damit befassen.

Stattdessen tauchen wir direkt ein, warum CSS-Variablen für die Erreichung dieser und anderer Fälle nützlich sind, dann fahren wir mit einer detaillierten Erklärung des *Wie* für verschiedene Fälle fort. Wir werden ein reales Beispiel von Grund auf Schritt für Schritt codieren und schließlich erhalten Sie etwas Augenfutter in Form von einigen weiteren Demos, die dieselbe Technik verwenden.

Legen wir also los!

Warum CSS-Variablen nützlich sind

Für mich ist das Beste an CSS-Variablen, dass sie die Tür zum stylen von Dingen auf logische, mathematische und mühelose Weise geöffnet haben.

Ein Beispiel hierfür ist die CSS-Varianten-Version des Yin-Yang-Loaders, den ich letztes Jahr codiert habe. Für diese Version erstellen wir die beiden Hälften mit den beiden Pseudo-Elementen des Lader-Elements.

Animated gif. The yin and yang symbol is rotating while its two lobes alternate increasing and decreasing in size - whenever one is increasing, it squishes the other one down.
Drehendes ☯-Symbol, bei dem sich die beiden Lappen vergrößern und verkleinern.

Wir verwenden dieselben Werte für background, border-color, transform-origin und animation-delay für die beiden Hälften. Diese Werte hängen alle von einer Schaltvariable --i ab, die anfangs auf 0 für beide Hälften (die Pseudo-Elemente) gesetzt ist, dann ändern wir sie auf 1 für die zweite Hälfte (das :after Pseudo-Element), wodurch die berechneten Werte all dieser Eigenschaften dynamisch modifiziert werden.

Ohne CSS-Variablen müssten wir all diese Eigenschaften (border-color, transform-origin, background, animation-delay) erneut auf dem :after Pseudo-Element setzen und riskierten, einen Tippfehler zu machen oder sogar zu vergessen, einige davon zu setzen.

Wie das Umschalten im Allgemeinen funktioniert

Umschalten zwischen Null und Nicht-Null-Wert

Im speziellen Fall des Yin-Yang-Loaders gehen alle Eigenschaften, die wir zwischen den beiden Hälften (Pseudo-Elementen) ändern, von einem Nullwert für den einen Zustand des Schalters zu einem Nicht-Nullwert für den anderen Zustand über.

Wenn wir möchten, dass unser Wert Null ist, wenn der Schalter ausgeschaltet ist (--i: 0) und Nicht-Null, wenn der Schalter eingeschaltet ist (--i: 1), dann multiplizieren wir ihn mit dem Schalterwert (var(--i)). So haben wir, wenn unser Nicht-Null-Wert sagen wir ein Winkelwert von 30deg sein soll:

  • wenn der Schalter aus ist (--i: 0), berechnet calc(var(--i)*30deg) zu 0*30deg = 0deg
  • wenn der Schalter ein ist (--i: 1), berechnet calc(var(--i)*30deg) zu 1*30deg = 30deg

Wenn wir jedoch möchten, dass unser Wert Nicht-Null ist, wenn der Schalter ausgeschaltet ist (--i: 0) und Null, wenn der Schalter eingeschaltet ist (--i: 1), dann multiplizieren wir ihn mit dem komplementären Wert des Schalters (1 - var(--i)). So haben wir für denselben Nicht-Null-Winkelwert von 30deg:

  • wenn der Schalter aus ist (--i: 0), berechnet calc((1 - var(--i))*30deg) zu (1 - 0)*30deg = 1*30deg = 30deg
  • wenn der Schalter ein ist (--i: 1), berechnet calc((1 - var(--i))*30deg) zu (1 - 1)*30deg = 0*30deg = 0deg

Sie können dieses Konzept unten sehen

Animated gif. Shows how changing the switch value from 0 to 1 changes the rotation of two boxes. The first box is rotated to 30deg when the switch is off (its value is 0) and not rotated or rotated to 0deg when the switch is on (its value is 1). This means we have a rotation value of calc((1 - var(--i))*30deg), where --i is the switch value. The second box is not rotated or rotated to 0deg when the switch is off (its value is 0) and rotated to 30deg when the switch is on (its value is 1). This means we have a rotation value of calc(var(--i)*30deg), with --i being the switch value.
Umschalten zwischen einem Null- und einem Nicht-Null-Wert (Live-Demo, keine Edge-Unterstützung aufgrund der Tatsache, dass calc() nicht für Winkelwerte funktioniert)

Für den speziellen Fall des Laders verwenden wir HSL-Werte für border-color und background-color. HSL steht für Farbton, Sättigung, Helligkeit und kann visuell am besten mit Hilfe eines Bikonus (der aus zwei Kegeln mit zusammengeklebten Basen besteht) dargestellt werden.

Two cones with their bases glued together in the middle, one vertex pointing down and one up. The hue is cyclic, distributed around the central (vertical) axis of the bicone. The saturation axis goes horizontally from the central axis towards the surface of the bicone - it's 0% right on the axis and 100% right on the surface. The lightness axis goes vertically from the black vertex to the white one - it's 0% at the black vertex and 100% at the white vertex.
HSL-Bikonus.

Die Farbtöne gehen um den Bikonus herum, wobei äquivalent zu 360° ist, um uns in beiden Fällen Rot zu geben.

Shows the red being at 0° (which is equivalent to 360° since the hue is cyclic), the yellow at 60°, the lime at 120°, the cyan at 180°, the blue at 240° and the magenta at 300°.
Farbtonrad.

Die Sättigung geht von 0% auf der vertikalen Achse des Bikonus bis zu 100% auf der Oberfläche des Bikonus. Wenn die Sättigung 0% beträgt (auf der vertikalen Achse des Bikonus), ist der Farbton nicht mehr wichtig; wir erhalten für alle Farbtöne in derselben horizontalen Ebene genau dasselbe Grau.

Die „gleiche horizontale Ebene“ bedeutet, die gleiche Helligkeit zu haben, die entlang der vertikalen Bikonusachse zunimmt, von 0% am schwarzen Bikonus-Scheitelpunkt bis zu 100% am weißen Bikonus-Scheitelpunkt. Wenn die Helligkeit entweder 0% oder 100% beträgt, sind weder Farbton noch Sättigung mehr wichtig – wir erhalten immer schwarz für einen Helligkeitswert von 0% und weiß für einen Helligkeitswert von 100%.

Da wir für unser ☯-Symbol nur Schwarz und Weiß benötigen, sind Farbton und Sättigung irrelevant, daher setzen wir sie auf Null und wechseln dann zwischen Schwarz und Weiß, indem wir die Helligkeit zwischen 0% und 100% umschalten.

.yin-yang {
  /* other styles that are irrelevant here */
  
  &:before, &:after {
    /* other styles that are irrelevant here */
    --i: 0;

    /* lightness of border-color when 
     * --i: 0 is (1 - 0)*100% = 1*100% = 100% (white)
     * --i: 1 is (1 - 1)*100% = 0*100% =   0% (black) */
    border: solid $d/6 hsl(0, 0%, calc((1 - var(--i))*100%));

    /* x coordinate of transform-origin when 
     * --i: 0 is 0*100% =   0% (left) 
     * --i: 1 is 1*100% = 100% (right) */
    transform-origin: calc(var(--i)*100%) 50%;

    /* lightness of background-color when 
     * --i: 0 is 0*100% =   0% (black) 
     * --i: 1 is 1*100% = 100% (white) */
    background: hsl(0, 0%, calc(var(--i)*100%));

    /* animation-delay when
     * --i: 0 is 0*-$t = 0s 
     * --i: 1 is 1*-$t = -$t */
    animation: s $t ease-in-out calc(var(--i)*#{-$t}) infinite alternate;
  }
	
  &:after { --i: 1 }
}

Beachten Sie, dass dieser Ansatz in Edge nicht funktioniert, da Edge calc()-Werte für animation-delay nicht unterstützt.

Aber was ist, wenn wir einen Nicht-Null-Wert haben wollen, wenn der Schalter ausgeschaltet ist (--i: 0) und einen anderen *anderen* Nicht-Null-Wert, wenn der Schalter eingeschaltet ist (--i: 1)?

Umschalten zwischen zwei Nicht-Null-Werten

Nehmen wir an, wir möchten, dass ein Element einen grauen background (#ccc) hat, wenn der Schalter aus ist (--i: 0) und einen orangen background (#f90), wenn der Schalter ein ist (--i: 1).

Das erste, was wir tun, ist, vom Hexadezimalformat zu einem besser handhabbaren Format wie rgb() oder hsl() zu wechseln.

Wir könnten dies manuell tun, entweder mit einem Werkzeug wie Lea Verous CSS Colors oder über die Entwicklertools. Wenn wir einen background für ein Element gesetzt haben, können wir durch die Formate wechseln, indem wir die Umschalttaste gedrückt halten, während wir auf das Quadrat (oder den Kreis) vor dem Wert in den Entwicklertools klicken. Dies funktioniert sowohl in Chrome als auch in Firefox, scheint aber in Edge nicht zu funktionieren.

Animiertes GIF. Zeigt, wie man über die Entwicklertools durch Formate (Hex/RGB/HSL) wechselt. Sowohl in Chrome als auch in Firefox tun wir dies, indem wir die Umschalttaste gedrückt halten und auf das Quadrat oder den Kreis vor dem <color data-recalc-dims= Wert klicken."/>
Formatänderung über die Entwicklertools.

Noch besser, wenn wir Sass verwenden, können wir die Komponenten mit den Funktionen red()/ green()/ blue() oder hue()/ saturation()/ lightness() extrahieren.

Während rgb() das bekanntere Format ist, bevorzuge ich hsl(), weil ich es intuitiver finde und es mir leichter fällt, durch Betrachten des Codes eine Vorstellung davon zu bekommen, was visuell zu erwarten ist.

Also extrahieren wir die drei Komponenten der hsl()-Äquivalente unserer beiden Werte ($c0: #ccc, wenn der Schalter aus ist, und $c1: #f90, wenn der Schalter ein ist) mit diesen Funktionen

$c0: #ccc;
$c1: #f90;

$h0: round(hue($c0)/1deg);
$s0: round(saturation($c0));
$l0: round(lightness($c0));

$h1: round(hue($c1)/1deg);
$s1: round(saturation($c1));
$l1: round(lightness($c1))

Beachten Sie, dass wir die Ergebnisse der Funktionen hue(), saturation() und lightness() gerundet haben, da sie viele Dezimalstellen zurückgeben können und wir unseren generierten Code sauber halten wollen. Wir haben auch das Ergebnis der Funktion hue() durch 1deg geteilt, da der zurückgegebene Wert in diesem Fall ein Gradwert ist und Edge nur einheitenlose Werte innerhalb der CSS hsl()-Funktion unterstützt. Normalerweise können wir bei der Verwendung von Sass Gradwerte haben, nicht nur einheitenlose für den Farbton innerhalb der hsl()-Funktion, da Sass dies als Sass hsl()-Funktion behandelt, die zu einer CSS hsl()-Funktion mit einem einheitenlosen Farbton kompiliert wird. Hier haben wir jedoch eine dynamische CSS-Variable, so dass Sass diese Funktion als die CSS hsl()-Funktion behandelt, die nicht in etwas anderes kompiliert wird. Wenn also der Farbton eine Einheit hat, wird diese nicht aus dem generierten CSS entfernt.

Nun haben wir das

  • wenn der Schalter aus ist (--i: 0), ist unser background
    hsl($h0, $s0, $l0)
  • wenn der Schalter ein ist (--i: 1), ist unser background
    hsl($h1, $s1, $l1)

Wir können unsere beiden Hintergründe so schreiben

  • wenn der Schalter aus ist (--i: 0),
    hsl(1*$h0 + 0*$h1, 1*$s0 + 0*$s1, 1*$l0 + 1*$l1)
  • wenn der Schalter ein ist (--i: 1),
    hsl(0*$h0 + 1*$h1, 0*$s0 + 1*$s1, 0*$l0 + 1*$l1)

Mit der Schaltvariable --i können wir die beiden Fälle vereinheitlichen

--j: calc(1 - var(--i));
background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), 
                calc(var(--j)*#{$s0} + var(--i)*#{$s1}), 
                calc(var(--j)*#{$l0} + var(--i)*#{$l1}))

Hier haben wir mit --j den komplementären Wert von --i bezeichnet (wenn --i 0 ist, ist --j 1 und wenn --i 1 ist, ist --j 0).

Animated gif. Shows how changing the switch value from 0 to 1 changes the background of a box. The background is grey (of hue $h0, saturation $s0 and lightness $l0) when the switch is turned off (its value is zero) and orange (of hue $h1, saturation $s1 and lightness $l1) when the switch is turned on (its value is 1). This means we have a hue value of calc(var(--j)*#{$h0} + var(--i)*#{$h1}), a saturation value of calc(var(--j)*#{$s0} + var(--i)*#{$s1}) and a lightness value of calc(var(--j)*#{$l0} + var(--i)*#{$l1})), where --i is the switch variable.
Umschalten zwischen zwei Hintergründen (Live-Demo)

Die obige Formel funktioniert zum Umschalten zwischen beliebigen zwei HSL-Werten. In diesem speziellen Fall können wir sie jedoch vereinfachen, da wir ein reines Grau haben, wenn der Schalter aus ist (--i: 0).

Reine Grautöne haben gleiche Rot-, Grün- und Blauwerte, wenn man das RGB-Modell betrachtet.

Beim Betrachten des HSL-Modells ist der Farbton irrelevant (unser Grau sieht für alle Farbtöne gleich aus), die Sättigung ist immer 0% und nur die Helligkeit bestimmt, wie hell oder dunkel unser Grau ist.

In dieser Situation können wir immer den Farbton des Nicht-Grau-Wertes (den wir für den „Ein“-Fall haben, $h1) beibehalten.

Da die Sättigung eines beliebigen Grauwerts (den wir für den „Aus“-Fall haben, $s0) immer 0% beträgt, ergibt die Multiplikation mit 0 oder 1 immer 0%. Da der Term var(--j)*#{$s0} in unserer Formel immer 0% ist, können wir ihn weglassen und unsere Sättigungsformel reduziert sich auf das Produkt zwischen der Sättigung des „Ein“-Falls $s1 und der Schaltvariable --i.

Damit bleibt die Helligkeit die einzige Komponente, bei der wir immer noch die vollständige Formel anwenden müssen.

--j: calc(1 - var(--i));
background: hsl($h1, 
                calc(var(--i)*#{$s1}), 
                calc(var(--j)*#{$l0} + var(--i)*#{d1l}))

Das oben kann in dieser Demo getestet werden.

Ebenso nehmen wir an, wir möchten, dass die font-size eines Textes 2rem beträgt, wenn unser Schalter aus ist (--i: 0) und 10vw, wenn der Schalter ein ist (--i: 1). Nach Anwendung derselben Methode haben wir

font-size: calc((1 - var(--i))*2rem + var(--i)*10vw)
Animated gif. Shows how changing the switch value from 0 to 1 changes the font-size.
Umschalten zwischen zwei Schriftgrößen (Live-Demo)

Okay, kommen wir nun zur Klärung eines weiteren Aspekts: Was genau bewirkt das Umschalten von Ein auf Aus oder umgekehrt?

Was löst das Umschalten aus

Wir haben hier ein paar Optionen.

Elementbasierte Umschaltung

Das bedeutet, dass der Schalter für bestimmte Elemente aus und für andere Elemente ein ist. Zum Beispiel kann dies durch Parität bestimmt werden. Nehmen wir an, wir möchten, dass alle geraden Elemente gedreht werden und einen orangen background anstelle des ursprünglichen grauen haben.

.box {
  --i: 0;
  --j: calc(1 - var(--i));
  transform: rotate(calc(var(--i)*30deg));
  background: hsl($h1, 
                  calc(var(--i)*#{$s1}), 
                  calc(var(--j)*#{$l0} + var(--i)*#{$l1}));
  
  &:nth-child(2n) { --i: 1 }
}
Screenshot. Shows a bunch of squares in a row, the even ones being rotated and having an orange background instead of the initial grey one. This is achieved by making both the transform and the background properties depend on a switch variable --i that changes with parity: it's 0 initially, but then we change it to 1 for even items.
Umschaltung ausgelöst durch Element-Parität (Live-Demo, nicht vollständig funktionsfähig in Edge, da calc() für Winkelwerte nicht funktioniert)

Im Paritätsfall schalten wir den Schalter für jedes zweite Element (:nth-child(2n)) ein, aber wir können ihn auch für jedes siebte Element (:nth-child(7n)) einschalten, für die ersten beiden Elemente (:nth-child(-n + 2)), für alle Elemente außer den ersten beiden und den letzten beiden (:nth-child(n + 3):nth-last-child(n + 3)). Wir können ihn auch nur für Überschriften oder nur für Elemente mit einem bestimmten Attribut einschalten.

Zustandsbasierte Umschaltung

Das bedeutet, dass der Schalter aus ist, wenn das Element selbst (oder ein Elternteil oder einer seiner vorherigen Geschwister) in einem Zustand ist, und aus, wenn es in einem anderen Zustand ist. In den interaktiven Beispielen im vorherigen Abschnitt wurde der Schalter umgelegt, wenn eine Checkbox vor unserem Element aktiviert oder deaktiviert wurde.

Wir können auch etwas wie einen weißen Link haben, der sich vergrößert und orange wird, wenn er fokussiert oder überfahren wird

$c: #f90;

$h: round(hue($c)/1deg);
$s: round(saturation($c));
$l: round(lightness($c));

a {
  --i: 0;
  transform: scale(calc(1 + var(--i)*.25));
  color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%));
  
  &:focus, &:hover { --i: 1 }
}

Da weiß jeder hsl()-Wert mit einer Helligkeit von 100% ist (Farbton und Sättigung sind irrelevant), können wir die Dinge vereinfachen, indem wir immer den Farbton und die Sättigung des :focus/:hover-Zustands beibehalten und nur die Helligkeit ändern.

Animated gif. Shows a white link that grows and turns orange when hovered or focused.
Umschaltung ausgelöst durch Zustandsänderung (Live-Demo, nicht vollständig funktionsfähig in Edge aufgrund von calc()-Werten, die nicht innerhalb von scale()-Funktionen unterstützt werden)

Medienabfrage-basierte Umschaltung

Eine weitere Möglichkeit ist, dass die Umschaltung durch eine Medienabfrage ausgelöst wird, z. B. wenn sich die Ausrichtung ändert oder von einem Ansichtsfensterbereich in einen anderen gewechselt wird.

Nehmen wir an, wir haben eine weiße Überschrift mit einer font-size von 1rem bis zu 320px, aber dann wird sie orange ($c) und die font-size wird 5vw und beginnt, sich mit der Ansichtsfensterbreite zu skalieren.

h5 {
  --i: 0;
  color: hsl($h, $s, calc(var(--i)*#{$l} + (1 - var(--i))*100%));
  font-size: calc(var(--i)*5vw + (1 - var(--i))*1rem);
  
  @media (min-width: 320px) { --i: 1 }
}
Animated gif. Shows a heading that's white and has a fixed font-size up to 320px, but as we resize the viewport above that, it becomes orange and its font-size starts scaling with the viewport width.
Umschaltung ausgelöst durch Ansichtsfensteränderung (Live-Demo)

Ein komplexeres Beispiel von Grund auf codieren

Das Beispiel, das wir hier sezieren, ist das der erweiterbaren Suche, die zu Beginn dieses Artikels gezeigt wurde und von diesem Pen inspiriert wurde, den Sie sich wirklich ansehen sollten, da der Code ziemlich genial ist.

Animated gif. Shows a green button with a magnifier icon. Clicking this button makes it slide right and its background to turn red while a text search field slides out of it to the left and the magnifier morphs into a close (crossmark) icon.
Erweiterbare Suche.

Beachten Sie, dass eine solche Suchleiste auf einer Website aus Sicht der Benutzerfreundlichkeit möglicherweise nicht die beste Idee ist, da man normalerweise erwarten würde, dass der Button, der der Suchleiste folgt, die Suche auslöst und nicht die Suchleiste schließt, aber es ist immer noch eine interessante Codierungsübung, weshalb ich sie hier seziere.

Zuerst war meine Idee, dies nur mit Formularelementen zu machen. Die HTML-Struktur sieht also so aus

<input id='search-btn' type='checkbox'/>
<label for='search-btn'>Show search bar</label>
<input id='search-bar' type='text' placeholder='Search...'/>

Was wir hier tun, ist, den Text input zunächst zu verstecken und ihn dann einzublenden, wenn die Checkbox davor aktiviert wird – tauchen wir ein, wie das funktioniert!

Zunächst verwenden wir ein einfaches Reset und setzen ein flex-Layout auf den Container unserer input- und label-Elemente. In unserem Fall ist dieser Container der body, es könnte aber auch ein anderes Element sein. Wir positionieren die Checkbox auch absolut und bewegen sie aus dem Blickfeld (außerhalb des Ansichtsfensters).

*, :before, :after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font: inherit
}

html { overflow-x: hidden }

body {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto;
  min-width: 400px;
  min-height: 100vh;
  background: #252525
}

[id='search-btn'] {
  position: absolute;
  left: -100vh
}

Bis hierhin ist alles gut…

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Na und? Wir müssen zugeben, es ist nicht aufregend, also machen wir weiter mit dem nächsten Schritt!

Wir verwandeln das label der Checkbox in einen großen runden grünen Button und bewegen dessen Textinhalt aus dem Blickfeld, indem wir einen großen negativen text-indent und overflow: hidden verwenden.

$btn-d: 5em;

/* same as before */

[for='search-btn'] {
  overflow: hidden;
  width: $btn-d;
  height: $btn-d;
  border-radius: 50%;
  box-shadow: 0 0 1.5em rgba(#000, .4);
  background: #d9eb52;
  text-indent: -100vw;
  cursor: pointer;
}

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Als Nächstes polieren wir die eigentliche Suchleiste, indem wir

  • ihr explizite Dimensionen geben
  • einen background für ihren normalen Zustand bereitstellen
  • einen anderen background und einen Glanz für ihren fokussierten Zustand definieren
  • die Ecken auf der linken Seite mit einem border-radius, der der halben Höhe entspricht, runden
  • Den Platzhalter etwas bereinigen
$btn-d: 5em;
$bar-w: 4*$btn-d;
$bar-h: .65*$btn-d;
$bar-r: .5*$bar-h;
$bar-c: #ffeacc;

/* same as before */

[id='search-bar'] {
  border: none;
  padding: 0 1em;
  width: $bar-w;
  height: $bar-h;
  border-radius: $bar-r 0 0 $bar-r;
  background: #3f324d;
  color: #fff;
  font: 1em century gothic, verdana, arial, sans-serif;
	
  &::placeholder {
    opacity: .5;
    color: inherit;
    font-size: .875em;
    letter-spacing: 1px;
    text-shadow: 0 0 1px, 0 0 2px
  }
	
  &:focus {
    outline: none;
    box-shadow: 0 0 1.5em $bar-c, 0 1.25em 1.5em rgba(#000, .2);
    background: $bar-c;
    color: #000;
  }
}

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Zu diesem Zeitpunkt fällt die rechte Kante der Suchleiste mit der linken Kante des Buttons zusammen. Wir wollen jedoch eine gewisse Überlappung – sagen wir, eine Überlappung, so dass die rechte Kante der Suchleiste mit der vertikalen Mittellinie des Buttons zusammenfällt. Da wir ein Flexbox-Layout mit align-items: center auf dem Container (in unserem Fall der body) haben, bleibt die aus unseren beiden Elementen (der Leiste und dem Button) bestehende Baugruppe horizontal mittig ausgerichtet, auch wenn wir eine margin für eines oder beide von ihnen zwischen diesen Elementen setzen. (Links vom äußersten linken Element oder rechts vom äußersten rechten Element ist eine andere Geschichte, aber darauf werden wir uns jetzt nicht einlassen.)

Illustration showing the bar plus button assembly in the initial state (bar's right edge coinciding with the button's left edge) vs. the overlap state (the bar's right edge coincides with the button's vertical midline). In both cases, the assembly is middle aligned.
Überlappung erzeugen, Ausrichtung beibehalten (Live-Demo).

Das ist eine Überlappung von .5*$btn-d abzüglich eines halben Button-Durchmessers, was dem Radius des Buttons entspricht. Wir setzen dies als negative margin-right für die Leiste. Wir passen auch den padding auf der rechten Seite der Leiste an, um die Überlappung auszugleichen

$btn-d: 5em;
$btn-r: .5*$btn-d;

/* same as before */

[id='search-bar'] {
  /* same as before */
  margin-right: -$btn-r;
  padding: 0 calc(#{$btn-r} + 1em) 0 1em;
}

Wir haben jetzt die Leiste und den Button in den Positionen für den erweiterten Zustand

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Außer dass die Leiste in der DOM-Reihenfolge dem Button folgt, also über ihm liegt, während wir eigentlich wollen, dass der Button oben liegt. Glücklicherweise ist das (zumindest vorerst) leicht zu beheben – später wird es nicht mehr ausreichen, aber lassen Sie uns ein Problem nach dem anderen angehen.

[for='search-btn'] {
  /* same as before */
  position: relative;
}

Da wir dem Button nun einen nicht-statischen position-Wert gegeben haben, liegt er über der Leiste

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

In diesem Zustand ist die Gesamtbreite der Leiste und Button-Baugruppe die Leistenbreite $bar-w plus der Button-Radius $btn-r (der die Hälfte des Button-Durchmessers $btn-d ist), da wir eine Überlappung für die Hälfte des Buttons haben. Im eingeklappten Zustand beträgt die Gesamtbreite der Baugruppe nur den Button-Durchmesser $btn-d.

Illustration showing the bar plus button assembly in the expanded state (the bar's right edge coincides with the button's vertical midline) and in the collapsed state (the bar is collapsed and the assembly is reduced to just the button). In both cases, the assembly is middle aligned.
Erweiterter vs. eingeklappter Zustand (Live).

Da wir die gleiche zentrale Achse beibehalten möchten, wenn wir vom erweiterten zum eingeklappten Zustand wechseln, müssen wir den Button um die Hälfte der Baugruppenbreite im erweiterten Zustand (.5*($bar-w + $btn-r)) abzüglich des Button-Radius ($btn-r) nach links verschieben.

Wir nennen diese Verschiebung $x und verwenden sie mit einem Minuszeichen für den Button (da wir den Button nach links verschieben und links die negative Richtung der x-Achse ist). Da wir wollen, dass die Leiste in den Button hineinklappt, setzen wir dieselbe Verschiebung $x für sie, aber in positiver Richtung (da wir die Leiste nach rechts von der x-Achse verschieben).

Wir befinden uns im eingeklappten Zustand, wenn die Checkbox nicht aktiviert ist, und im erweiterten Zustand, wenn sie es ist. Das bedeutet, dass unsere Leiste und unser Button mit einer CSS-transform verschoben werden, wenn die Checkbox nicht aktiviert ist, und in der Position, in der wir sie derzeit haben (keine transform), wenn die Checkbox aktiviert ist.

Um dies zu tun, setzen wir eine Variable --i auf die Elemente, die unserer Checkbox folgen – den Button (erstellt mit dem label für die Checkbox) und die Suchleiste. Diese Variable ist 0 im eingeklappten Zustand (wenn beide Elemente verschoben sind und die Checkbox nicht aktiviert ist) und 1 im erweiterten Zustand (wenn unsere Leiste und unser Button in den Positionen sind, die sie derzeit einnehmen, keine Verschiebung, und die Checkbox aktiviert ist).

$x: .5*($bar-w + $btn-r) - $btn-r;

[id='search-btn'] {
  position: absolute;
  left: -100vw;
	
  ~ * {
    --i: 0;
    --j: calc(1 - var(--i)) /* 1 when --i is 0, 0 when --i is 1 */
  }
	
  &:checked ~ * { --i: 1 }
}

[for='search-btn'] {
  /* same as before */
  /* if --i is 0, --j is 1 => our translation amount is -$x
   * if --i is 1, --j is 0 => our translation amount is 0 */
  transform: translate(calc(var(--j)*#{-$x}));
}

[id='search-bar'] {
  /* same as before */
  /* if --i is 0, --j is 1 => our translation amount is $x
   * if --i is 1, --j is 0 => our translation amount is 0 */
  transform: translate(calc(var(--j)*#{$x}));
}

Und nun haben wir etwas Interaktives! Das Klicken auf den Button schaltet den Zustand der Checkbox um (da der Button mit dem label der Checkbox erstellt wurde).

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Außer dass der Button nun etwas schwierig zu klicken ist, da er wieder unter dem Texteingabefeld liegt (weil wir eine transform auf die Leiste gesetzt haben und dies einen Stapelkontext etabliert). Die Behebung ist ziemlich einfach – wir müssen dem Button einen z-index hinzufügen, und das bewegt ihn über die Leiste.

[for='search-btn'] {
  /* same as before */
  z-index: 1;
}

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Aber wir haben immer noch ein weiteres, größeres Problem: Wir sehen die Leiste, die auf der rechten Seite unter dem Button hervorkommt. Um dies zu beheben, setzen wir clip-path mit einem inset()-Wert auf die Leiste. Dies gibt einen Clipping-Rechteck mit den Abständen vom oberen, rechten, unteren und linken Rand des border-box des Elements an. Alles außerhalb dieses Clipping-Rechtecks wird abgeschnitten und nur das Innere wird angezeigt.

Illustration showing what the four values of the inset() function represent. The first one is the offset of the top edge of the clipping rectangle with respect to the top edge of the border-box. The second one is the offset of the right edge of the clipping rectangle with respect to the right edge of the border-box. The third one is the offset of the bottom edge of the clipping rectangle with respect to the bottom edge of the border-box. The fourth one is the offset of the left edge of the clipping rectangle with respect to the left edge of the border-box. Everything outside the
Wie die inset()-Funktion funktioniert (Live).

In der obigen Abbildung ist jeder Abstand von den Rändern des Border-Box nach innen gerichtet. In diesem Fall sind sie positiv. Aber sie können auch nach außen gehen, in diesem Fall sind sie negativ und die entsprechenden Ränder des Clipping-Rechtecks liegen außerhalb des border-box des Elements.

Zuerst denken Sie vielleicht, dass wir dafür niemals einen Grund haben werden, aber in unserem speziellen Fall tun wir das!

Wir wollen, dass die Abstände von oben (dt), unten (db) und links (dl) negativ und groß genug sind, um den box-shadow zu enthalten, der im :focus-Zustand außerhalb des border-box des Elements hinausragt, da wir nicht wollen, dass er abgeschnitten wird. Die Lösung ist also, ein Clipping-Rechteck mit Rändern außerhalb des border-box des Elements in diesen drei Richtungen zu erstellen.

Der Abstand von rechts (dr) ist die volle Leistenbreite $bar-w abzüglich eines Button-Radius $btn-r im eingeklappten Fall (Checkbox nicht aktiviert, --i: 0) und 0 im erweiterten Fall (Checkbox aktiviert, --i: 1).

$out-d: -3em;

[id='search-bar'] {
  /* same as before */
  clip-path: inset($out-d calc(var(--j)*#{$bar-w - $btn-r}) $out-d $out-d);
}

Wir haben nun eine Suchleiste und eine Button-Baugruppe, die sich beim Klicken auf den Button erweitert und zusammenzieht.

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Da wir keine abrupte Änderung zwischen den beiden Zuständen wünschen, verwenden wir eine transition

[id='search-btn'] {
  /* same as before */
	
  ~ * {
    /* same as before */
    transition: .65s;
  }
}

Wir möchten auch, dass der background unseres Buttons im eingeklappten Zustand grün ist (Checkbox nicht aktiviert, --i: 0) und im erweiterten Zustand pink (Checkbox aktiviert, --i: 1). Dafür verwenden wir dieselbe Technik wie zuvor

[for='search-btn'] {
  /* same as before */
  $c0: #d9eb52; // green for collapsed state
  $c1: #dd1d6a; // pink for expanded state
  $h0: round(hue($c0)/1deg);
  $s0: round(saturation($c0));
  $l0: round(lightness($c0));
  $h1: round(hue($c1)/1deg);
  $s1: round(saturation($c1));
  $l1: round(lightness($c1));
  background: hsl(calc(var(--j)*#{$h0} + var(--i)*#{$h1}), 
                  calc(var(--j)*#{$s0} + var(--i)*#{$s1}), 
                  calc(var(--j)*#{$l0} + var(--i)*#{$l1}));
}

Jetzt kommen wir der Sache näher!

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Was wir noch tun müssen, ist, das Icon zu erstellen, das sich im eingeklappten Zustand von einer Lupe zu einem „x“ im erweiterten Zustand morphisiert, um eine Schließaktion anzuzeigen. Das machen wir mit den Pseudo-Elementen :before und :after. Wir beginnen damit, einen Durchmesser für die Lupe und den Anteil dieses Durchmessers, den die Breite der Icon-Linien ausmacht, festzulegen.

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;

Wir positionieren beide Pseudo-Elemente absolut in der Mitte des Buttons und berücksichtigen deren Dimensionen. Dann lassen wir sie die transition ihres Elternelements inheriten. Wir geben dem :before einen background, da dies der Griff unserer Lupe sein wird, machen das :after rund mit border-radius und geben ihm einen nach innen versetzten box-shadow.

[for='search-btn'] {
  /* same as before */
	
  &:before, &:after {
    position: absolute;
    top: 50%; left: 50%;
    margin: -.5*$ico-d;
    width: $ico-d;
    height: $ico-d;
    transition: inherit;
    content: ''
  }
	
  &:before {
    margin-top: -.4*$ico-w;
    height: $ico-w;
    background: currentColor
  }
  
  &:after {
    border-radius: 50%;
    box-shadow: 0 0 0 $ico-w currentColor
  } 
}

Wir können nun die Lupe-Komponenten auf dem Button sehen

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Um unser Icon mehr wie eine Lupe aussehen zu lassen, translaten wir beide seiner Komponenten nach außen um ein Viertel des Lupe-Durchmessers. Das bedeutet, den Griff nach rechts, in positiver Richtung der x-Achse um .25*$ico-d zu verschieben, und den Hauptteil nach links, in negativer Richtung der x-Achse um denselben Betrag .25*$ico-d.

Wir scalen auch den Griff (das :before Pseudo-Element) horizontal auf die Hälfte seiner width in Bezug auf seine rechte Kante (was eine transform-origin von 100% entlang der x-Achse bedeutet).

Wir wollen, dass dies nur im eingeklappten Zustand geschieht (Checkbox nicht aktiviert, --i ist 0 und folglich --j ist 1), also multiplizieren wir die Translationsbeträge mit --j und verwenden --j auch, um den Skalierungsfaktor zu konditionieren

[for='search-btn'] {
  /* same as before */
	
  &:before {
    /* same as before */
    height: $ico-w;
    transform: 
      /* collapsed: not checked, --i is 0, --j is 1
       * translation amount is 1*.25*$d = .25*$d
       * expanded: checked, --i is 1, --j is 0
       * translation amount is 0*.25*$d = 0 */
      translate(calc(var(--j)*#{.25*$ico-d})) 
      /* collapsed: not checked, --i is 0, --j is 1
       * scaling factor is 1 - 1*.5 = 1 - .5 = .5
       * expanded: checked, --i is 1, --j is 0
       * scaling factor is 1 - 0*.5 = 1 - 0 = 1 */
      scalex(calc(1 - var(--j)*.5))
  }
  
  &:after {
    /* same as before */
    transform: translate(calc(var(--j)*#{-.25*$ico-d}))
  } 
}

Wir haben nun das Lupen-Icon im eingeklappten Zustand

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Da wir beide Icon-Komponenten um 45deg drehen möchten, fügen wir diese Drehung dem Button selbst hinzu

[for='search-btn'] {
  /* same as before */
  transform: translate(calc(var(--j)*#{-$x})) rotate(45deg);
}

Nun haben wir das gewünschte Aussehen für den eingeklappten Zustand

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Das lässt den erweiterten Zustand, in dem wir das runde :after Pseudo-Element in eine Linie verwandeln müssen, immer noch offen. Das machen wir, indem wir es entlang der x-Achse skalieren und seinen border-radius von 50% auf 0% bringen. Der verwendete Skalierungsfaktor ist das Verhältnis zwischen der gewünschten Linienbreite $ico-w und dem Durchmesser $ico-d des Kreises, den es im eingeklappten Zustand bildet. Wir haben dieses Verhältnis $ico-f genannt.

Da wir dies nur im erweiterten Zustand tun wollen, wenn die Checkbox aktiviert ist und --i 1 ist, machen wir sowohl den Skalierungsfaktor als auch den border-radius von --i und --j abhängig

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;

[for='search-btn'] {
  /* same as before */
	
  &:after{
    /* same as before */
    /* collapsed: not checked, --i is 0, --j is 1
     * border-radius is 1*50% = 50%
     * expanded: checked, --i is 1, --j is 0
     * border-radius is 0*50% = 0 */
    border-radius: calc(var(--j)*50%);
    transform: 
      translate(calc(var(--j)*#{-.25*$ico-d})) 
      /* collapsed: not checked, --i is 0, --j is 1
       * scaling factor is 1 + 0*$ico-f = 1
       * expanded: checked, --i is 1, --j is 0
       * scaling factor is 0 + 1*$ico-f = $ico-f */
      scalex(calc(1 - var(--j)*.5))
  }
}

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Hm, fast, aber nicht ganz. Das Skalieren hat auch unseren inneren box-shadow entlang der x-Achse verkleinert, also beheben wir das mit einem zweiten inneren Schatten, den wir nur im erweiterten Zustand (wenn die Checkbox aktiviert ist und --i 1 ist) erhalten und dessen Streuung und Alpha daher von --i abhängen

$ico-d: .5*$bar-h;
$ico-f: .125;
$ico-w: $ico-f*$ico-d;

[for='search-btn'] {
  /* same as before */
  --hsl: 0, 0%, 0%;
  color: HSL(var(--hsl));
	
  &:after{
    /* same as before */
    box-shadow: 
      inset 0 0 0 $ico-w currentcolor, 
      /* collapsed: not checked, --i is 0, --j is 1
       * spread radius is 0*.5*$ico-d = 0
       * alpha is 0
       * expanded: checked, --i is 1, --j is 0
       * spread radius is 1*.5*$ico-d = .5*$ico-d
       * alpha is 1 */
      inset 0 0 0 calc(var(--i)*#{.5*$ico-d}) HSLA(var(--hsl), var(--i))
  }
}

Das ergibt unser Endergebnis!

Siehe den Pen von thebabydino (@thebabydino) auf CodePen.

Ein paar weitere schnelle Beispiele

Die folgenden sind ein paar weitere Demos, die dieselbe Technik verwenden. Wir werden diese nicht von Grund auf neu erstellen – wir werden nur die grundlegenden Ideen dahinter durchgehen.

Responsive Banner

On the left, a screenshot of the wide screen scenario. In the middle, a screenshot of the normal screen scenario. On the right, a screenshot of the narrow screen scenario.
Screenshot-Collage (Live-Demo, in Edge nicht voll funktionsfähig, da eine calc()-Angabe für font-size verwendet wird).

In diesem Fall sind unsere eigentlichen Elemente die kleineren Rechtecke vorne, während die Zahlenquadrate und die größeren Rechtecke hinten mit den Pseudo-Elementen :before bzw. :after erstellt werden.

Die Hintergründe der Zahlenquadrate sind individuell und werden mit einer Stopplisten-Variable --slist gesetzt, die für jedes Element unterschiedlich ist.

<p style='--slist: #51a9ad, #438c92'><!-- 1st paragraph text --></p>
<p style='--slist: #ebb134, #c2912a'><!-- 2nd paragraph text --></p>
<p style='--slist: #db4453, #a8343f'><!-- 3rd paragraph text --></p>
<p style='--slist: #7eb138, #6d982d'><!-- 4th paragraph text --></p>

Die Dinge, die die Stile der Banner beeinflussen, sind die Parität und ob wir uns im weiten, normalen oder schmalen Fall befinden. Diese ergeben unsere Schaltvariablen.

html {
  --narr: 0;
  --comp: calc(1 - var(--narr));
  --wide: 1;
	
  @media (max-width: 36em) { --wide: 0 }
	
  @media (max-width: 20em) { --narr: 1 }
}

p {
  --parity: 0;
  
  &:nth-child(2n) { --parity: 1 }
}

Die Zahlenquadrate sind absolut positioniert und ihre Platzierung hängt von der Parität ab. Wenn der --parity-Schalter ausgeschaltet ist (0), dann sind sie links. Wenn er eingeschaltet ist (1), dann sind sie rechts.

Ein Wert von left: 0% richtet die linke Kante des Zahlenquadrats an der linken Kante seines Elternelements aus, während ein Wert von left: 100% die linke Kante an der rechten Kante des Elternelements ausrichtet.

Um die rechte Kante des Zahlenquadrats an der rechten Kante seines Elternelements ausgerichtet zu haben, müssen wir seine eigene Breite von dem vorherigen Wert von 100% abziehen. (Denken Sie daran, dass %-Werte bei Offsets sich auf die Abmessungen des Elternelements beziehen.)

left: calc(var(--parity)*(100% - #{$num-d}))

…wobei $num-d die Größe des Nummerquadrats ist.

Im weiten Bildschirmfall verschieben wir die Nummerierung auch um 1em nach außen – das bedeutet, wir ziehen 1em vom bisherigen Offset für ungerade Elemente (bei ausgeschaltetem --parity-Schalter) ab und addieren 1em zum bisherigen Offset für gerade Elemente (bei eingeschaltetem --parity-Schalter).

Nun stellt sich die Frage… wie wechseln wir das Vorzeichen? Der einfachste Weg ist die Verwendung der Potenzen von -1. Leider haben wir keine Potenzfunktion (oder einen Potenzoperator) in CSS, obwohl diese in diesem Fall immens nützlich wäre.

/*
 * for --parity: 0, we have pow(-1, 0) = +1
 * for --parity: 1, we have pow(-1, 1) = -1
 */
pow(-1, var(--parity))

Das bedeutet, wir müssen es mit dem arbeiten, was wir haben (Addition, Subtraktion, Multiplikation und Division), und das führt zu einer seltsamen kleinen Formel… aber, hey, es funktioniert!

/*
 * for --parity: 0, we have 1 - 2*0 = 1 - 0 = +1
 * for --parity: 1, we have 1 - 2*1 = 1 - 2 = -1
 */
--sign: calc(1 - 2*var(--parity))

Auf diese Weise wird unsere endgültige Formel für den linken Offset, die sowohl die Parität als auch ob wir uns im weiten Fall befinden (--wide: 1) oder nicht (--wide: 0), berücksichtigt.

left: calc(var(--parity)*(100% - #{$num-d}) - var(--wide)*var(--sign)*1em)

Wir steuern auch die width der Absätze mit diesen Variablen und max-width, da wir eine Obergrenze wünschen und diese im schmalen Fall (--narr: 1) nur ihr Elternelement horizontal vollständig abdecken.

width: calc(var(--comp)*80% + var(--narr)*100%);
max-width: 35em;

Die font-size hängt auch davon ab, ob wir uns im schmalen Fall befinden (--narr: 1) oder nicht (--narr: 0).

calc(.5rem + var(--comp)*.5rem + var(--narr)*2vw)

…und so auch die horizontalen Offsets für das Pseudo-Element :after (das größere Rechteck hinten), da diese im schmalen Fall (--narr: 1) 0 und sonst ein Offset $off-x ungleich Null sind (--narr: 0).

right: calc(var(--comp)*#{$off-x}); 
left: calc(var(--comp)*#{$off-x});

Hover- und Fokus-Effekte

Animated gif. Shows red diagonal sliding bands covering the white button underneath the black text on hover/focus. On mouseout/ blur, the bands slide out the other way, not the way they entered.
Effekt-Aufnahme (Live-Demo, in Edge nicht voll funktionsfähig wegen verschachteltem calc()-Bug).

Dieser Effekt wird mit einem Link-Element und seinen beiden Pseudo-Elementen erzeugt, die sich in den :hover- und :focus-Zuständen diagonal verschieben. Die Abmessungen des Links und seiner Pseudo-Elemente sind fest und entsprechen der Diagonale ihres Elternelements $btn-d (berechnet als Hypotenuse eines rechtwinkligen Dreiecks aus Breite und Höhe) horizontal und der height des Elternelements vertikal.

Das :before ist so positioniert, dass seine untere linke Ecke mit der seines Elternelements zusammenfällt, während das :after so positioniert ist, dass seine obere rechte Ecke mit der seines Elternelements zusammenfällt. Da beide die gleiche height wie ihr Elternelement haben sollen, wird die vertikale Platzierung durch Setzen von top: 0 und bottom: 0 gelöst. Die horizontale Platzierung erfolgt auf genau die gleiche Weise wie im vorherigen Beispiel, wobei --i als Schaltvariable verwendet wird, die zwischen den beiden Pseudo-Elementen ihren Wert ändert, und --j, ihre Ergänzung (calc(1 - var(--i))).

left: calc(var(--j)*(100% - #{$btn-d}))

Wir setzen die transform-origin des :before auf seine linke untere Ecke (0% 100%) und die des :after auf seine rechte obere Ecke (100% 0%), wiederum mit Hilfe des Schalters --i und seiner Ergänzung --j.

transform-origin: calc(var(--j)*100%) calc(var(--i)*100%)

Wir drehen beide Pseudo-Elemente um den Winkel zwischen der Diagonalen und der Horizontalen $btn-a (ebenfalls berechnet aus dem Dreieck aus Höhe und Breite als Arkustangens des Verhältnisses zwischen beiden). Mit dieser Drehung treffen sich die horizontalen Kanten entlang der Diagonalen.

Anschließend verschieben wir sie um ihre eigene Breite nach außen. Das bedeutet, wir verwenden für jeden der beiden ein anderes Vorzeichen, wieder abhängig von der Schaltvariable, die zwischen :before und :after ihren Wert ändert, genau wie im vorherigen Beispiel mit den Bannern.

transform: rotate($btn-a) translate(calc((1 - 2*var(--i))*100%))

In den :hover- und :focus-Zuständen muss diese Translation auf 0 zurückkehren. Das bedeutet, wir multiplizieren den Betrag der obigen Translation mit der Ergänzung --q der Schaltvariable --p, die im Normalzustand 0 und im :hover- oder :focus-Zustand 1 ist.

transform: rotate($btn-a) translate(calc(var(--q)*(1 - 2*var(--i))*100%))

Um die Pseudo-Elemente beim Maus-aus oder beim Verlassen des Fokus auf die andere Weise (nicht zurück, woher sie kamen) herausgleiten zu lassen, setzen wir die Schaltvariable --i auf den Wert von --p für :before und auf den Wert von --q für :after, kehren das Vorzeichen der Translation um und stellen sicher, dass wir nur die transform-Eigenschaft überblenden.

Responsive Infografik

On the left, a screenshot of the wide screen scenario. We have a three row, two column grid with the third row collapsed (height zero). The first level heading occupies either the column on the right (for odd items) or the one on the left (for even items). The second level heading is on the other column and on the first row, while the paragraph text is below the second level heading on the second row. On the right, a screenshot of the narrower scenario. In this case, the third row has a height enough to fit the paragraph text, but the second column is collapsed. The first and second level heading occupy the first and second row respectively.
Screenshot-Collage mit hervorgehobenen Gitterlinien und Abständen (Live-Demo, kein Edge-Support aufgrund von CSS-Variablen und calc()-Bugs).

In diesem Fall haben wir ein Drei-Reihen-, Zwei-Spalten-Gitter für jedes Element (article-Element), wobei die dritte Reihe im weiten Bildschirmfall zusammengeklappt ist und die zweite Spalte im schmalen Bildschirmfall. Im weiten Bildschirmfall hängen die Breiten der Spalten von der Parität ab. Im schmalen Bildschirmfall erstreckt sich die erste Spalte über die gesamte Content-Box des Elements und die zweite hat eine Breite von 0. Wir haben auch einen Abstand zwischen den Spalten, aber nur im weiten Bildschirmfall.

// formulas for the columns in the wide screen case, where
// $col-a-wide is for second level heading + paragraph
// $col-b-wide is for the first level heading
$col-1-wide: calc(var(--q)*#{$col-a-wide} + var(--p)*#{$col-b-wide});
$col-2-wide: calc(var(--q)*#{$col-b-wide} + var(--p)*#{$col-a-wide});

// formulas for the general case, combining the wide and normal scenarios
$row-1: calc(var(--i)*#{$row-1-wide} + var(--j)*#{$row-1-norm});
$row-2: calc(var(--i)*#{$row-2-wide} + var(--j)*#{$row-2-norm});
$row-3: minmax(0, auto);
$col-1: calc(var(--i)*#{$col-1-wide} + var(--j)*#{$col-1-norm});
$col-2: calc(var(--i)*#{$col-2-wide});

$art-g: calc(var(--i)*#{$art-g-wide});

html {
  --i: var(--wide, 1); // 1 in the wide screen case
  --j: calc(1 - var(--i));

  @media (max-width: $art-w-wide + 2rem) { --wide: 0 }
}

article {
  --p: var(--parity, 0);
  --q: calc(1 - var(--p));
  --s: calc(1 - 2*var(--p));
  display: grid;
  grid-template: #{$row-1} #{$row-2} #{$row-3}/ #{$col-1} #{$col-2};
  grid-gap: 0 $art-g;
  grid-auto-flow: column dense;

  &:nth-child(2n) { --parity: 1 }
}

Da wir grid-auto-flow: column dense gesetzt haben, können wir uns damit begnügen, nur die Überschrift erster Ordnung so einzustellen, dass sie im weiten Bildschirmfall eine gesamte Spalte (die zweite für ungerade Elemente und die erste für gerade Elemente) abdeckt, und die Überschrift zweiter Ordnung und den Absatztext die ersten freien verfügbaren Zellen füllen lassen.

// wide case, odd items: --i is 1, --p is 0, --q is 1
// we're on column 1 + 1*1 = 2
// wide case, even items: --i is 1, --p is 1, --q is 0
// we're on column 1 + 1*0 = 1
// narrow case: --i is 0, so var(--i)*var(--q) is 0 and we're on column 1 + 0 = 1
grid-column: calc(1 + var(--i)*var(--q));

// always start from the first row
// span 1 + 2*1 = 3 rows in the wide screen case (--i: 1)
// span 1 + 2*0 = 1 row otherwise (--i: 0)
grid-row: 1/ span calc(1 + 2*var(--i));

Für jedes Element hängen einige andere Eigenschaften davon ab, ob wir uns im weiten Bildschirmfall befinden oder nicht.

Die vertikalen margin-, vertikalen und horizontalen padding-Werte, box-shadow-Offsets und der Weichzeichner sind im weiten Bildschirmfall größer.

$art-mv: calc(var(--i)*#{$art-mv-wide} + var(--j)*#{$art-mv-norm});
$art-pv: calc(var(--i)*#{$art-pv-wide} + var(--j)*#{$art-p-norm});
$art-ph: calc(var(--i)*#{$art-ph-wide} + var(--j)*#{$art-p-norm});
$art-sh: calc(var(--i)*#{$art-sh-wide} + var(--j)*#{$art-sh-norm});

article {
  /* other styles */
  margin: $art-mv auto;
  padding: $art-pv $art-ph;
  box-shadow: $art-sh $art-sh calc(3*#{$art-sh}) rgba(#000, .5);
}

Wir haben eine nicht-Null-border-width und border-radius im weiten Bildschirmfall.

$art-b: calc(var(--i)*#{$art-b-wide});
$art-r: calc(var(--i)*#{$art-r-wide});

article {
  /* other styles */
  border: solid $art-b transparent;
  border-radius: $art-r;
}

Im weiten Bildschirmfall begrenzen wir die width der Elemente, lassen sie aber ansonsten 100% sein.

$art-w: calc(var(--i)*#{$art-w-wide} + var(--j)*#{$art-w-norm});

article {
  /* other styles */
  width: $art-w;
}

Die Richtung des padding-box-Gradierten ändert sich ebenfalls mit der Parität.

background: 
  linear-gradient(calc(var(--s)*90deg), #e6e6e6, #ececec) padding-box, 
  linear-gradient(to right bottom, #fff, #c8c8c8) border-box;

In ähnlicher Weise hängen margin, border-width, padding, width, border-radius, die Richtung des background-Gradierten, font-size oder line-height für die Überschriften und den Absatztext davon ab, ob wir uns im weiten Bildschirmfall befinden oder nicht (und im Fall des border-radius der Überschrift erster Ordnung oder der Richtung des background-Gradierten auch von der Parität).