Verschiedene Methoden, um eine Box zu vergrößern und dabei den abgerundeten Eckenradius beizubehalten

Avatar of Ana Tudor
Ana Tudor am

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

Ich habe kürzlich eine interessante Änderung auf CodePen bemerkt: Wenn man mit der Maus über die Pens auf der Homepage fährt, erweitert sich im Hintergrund ein Rechteck mit abgerundeten Ecken.

Animated gif recording the CodePen expanding box effect on hover.
Effekt der sich erweiternden Box auf der CodePen-Homepage.

Als neugieriges Wesen, das ich nun mal bin, musste ich nachsehen, wie das funktioniert! Es stellt sich heraus, dass das Rechteck im Hintergrund ein absolut positioniertes ::after Pseudoelement ist.

Collage. On the left side, there is a DevTools screenshot showing the initial styles applied on the ::after pseudo-element. The relevant ones are those making it absolutely positioned with an offset of 1rem from the top and left and with an offset of -1rem from the right and bottom. On the right side, we have an illustration of these styles, showing the parent element box, the ::after box and the offsets between their edges.
Ursprüngliche ::after-Stile. Ein positiver Offset geht von der padding-Grenze des Elternteils nach innen, während ein negativer nach außen geht.

Bei :hover werden seine Offsets überschrieben und in Kombination mit der transition erhalten wir den Effekt der sich erweiternden Box.

Collage. On the left side, there is a DevTools screenshot showing the :hover styles applied on the ::after pseudo-element. These are all offsets overriding the initial ones and making the boundary of the ::after shift outwards by 2rem in all directions except the right. On the right side, we have an illustration of these styles, showing the parent element box, the ::after box and the offsets between their edges.
Die ::after-Stile bei :hover.

Die Eigenschaft right hat sowohl in den ursprünglichen als auch in den :hover-Regelsätzen den gleichen Wert (-1rem), daher ist es unnötig, sie zu überschreiben. Alle anderen Offsets bewegen sich um 2rem nach außen (von 1rem auf -1rem für die top- und left-Offsets und von -1rem auf -3rem für den bottom-Offset).

Eine Sache, die hier auffällt, ist, dass das ::after-Pseudoelement einen border-radius von 10px hat, der beim Erweitern erhalten bleibt. Das brachte mich zum Nachdenken darüber, welche Methoden wir haben, um (Pseudo-)Elemente zu erweitern/schrumpfen und dabei ihren border-radius zu erhalten. Wie viele fallen Ihnen ein? Lassen Sie es mich wissen, wenn Sie Ideen haben, die unten nicht aufgeführt sind, wo wir uns eine Reihe von Optionen ansehen und sehen, welche für welche Situation am besten geeignet ist.

Ändern der Offsets

Dies ist die Methode, die auf CodePen verwendet wird, und sie funktioniert aus einer Reihe von Gründen in dieser speziellen Situation wirklich gut. Erstens hat sie eine hervorragende Unterstützung. Sie funktioniert auch, wenn das sich erweiternde (Pseudo-)Element responsiv ist, ohne feste Abmessungen, und gleichzeitig der Erweiterungsbetrag fest ist (ein rem-Wert). Sie funktioniert auch für die Erweiterung in mehr als zwei Richtungen (top, bottom und left in diesem speziellen Fall).

Es gibt jedoch ein paar Vorbehalte, die wir beachten müssen.

Erstens kann unser sich erweiterndes Element nicht position: static haben. Dies ist im Kontext des CodePen-Anwendungsfalls kein Problem, da das ::after-Pseudoelement sowieso absolut positioniert sein muss, um unter dem Rest des Inhalts seines Elternteils platziert zu werden.

Zweitens kann eine Übertreibung bei Offset-Animationen (sowie generell bei der Animation von Eigenschaften, die das Layout mit Box-Eigenschaften beeinflussen, wie Offsets, Margins, Randbreiten, Paddings oder Abmessungen) die Leistung negativ beeinflussen. Auch hier ist das kein Problem, wir haben nur einen kleinen transition bei :hover, keine große Sache.

Ändern der Abmessungen

Anstatt Offsets zu ändern, könnten wir stattdessen Abmessungen ändern. Dies ist jedoch eine Methode, die funktioniert, wenn wir möchten, dass sich unser (Pseudo-)Element in maximal zwei Richtungen erweitert. Andernfalls müssen wir auch Offsets ändern. Um dies besser zu verstehen, betrachten wir die CodePen-Situation, in der wir möchten, dass sich unsere ::after-Pseudoelemente in drei Richtungen (top, bottom und left) erweitern.

Die relevanten ursprünglichen Größeninformationen sind folgende:

.single-item::after {
  top: 1rem;
  right: -1rem;
  bottom: -1rem;
  left: 1rem;
}

Da sich gegenüberliegende Offsets (die Paare topbottom und leftright) gegenseitig aufheben (1rem - 1rem = 0), ergeben sich daraus die Abmessungen des Pseudoelements, die denen seines Elternteils entsprechen (oder 100% der Abmessungen des Elternteils).

Wir können das obige also umschreiben als:

.single-item::after {
  top: 1rem;
  right: -1rem;
  width: 100%;
  height: 100%;
}

Bei :hover erhöhen wir die width um 2rem nach links und die height um 4rem, 2rem nach oben und 2rem nach unten. Aber nur das Schreiben von:

.single-item::after {
  width: calc(100% + 2rem);
  height: calc(100% + 4rem);
}

… reicht nicht aus, da dies die height um 4rem in Richtung nach unten erhöht, anstatt sie um 2rem nach oben und 2rem nach unten zu erhöhen. Das folgende Demo verdeutlicht dies (setzen Sie :focus auf die Elemente oder fahren Sie mit der Maus darüber, um zu sehen, wie sich das ::after-Pseudoelement erweitert).

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

Wir müssten die Eigenschaft top ebenfalls aktualisieren, um den gewünschten Effekt zu erzielen:

.single-item::after {
  top: -1rem;
  width: calc(100% + 2rem);
  height: calc(100% + 4rem);
}

Was funktioniert, wie unten zu sehen ist:

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

Aber, ehrlich gesagt, fühlt sich das weniger wünschenswert an als das reine Ändern von Offsets.

Das Ändern von Abmessungen ist jedoch eine gute Lösung für eine andere Art von Situation, z. B. wenn wir Balken mit abgerundeten Ecken haben möchten, die sich in einer einzigen Richtung erweitern/schrumpfen.

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

Beachten Sie, dass, wenn wir keine abgerundeten Ecken zum Beibehalten hätten, die bessere Lösung die gerichtete Skalierung über die transform-Eigenschaft wäre.

Ändern von Padding/Randbreite

Ähnlich wie beim Ändern der Abmessungen können wir das padding oder die border-width (für einen border, der transparent ist) ändern. Beachten Sie, dass wir, genau wie beim Ändern der Abmessungen, auch die Offsets aktualisieren müssen, wenn die Box in mehr als zwei Dimensionen erweitert wird.

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

In der obigen Demo stellt die rosafarbene Box die content-box des ::after-Pseudoelements dar, und Sie können sehen, dass sie gleich groß bleibt, was für diesen Ansatz wichtig ist.

Um zu verstehen, warum das wichtig ist, betrachten wir diese weitere Einschränkung: Wir müssen auch die Box-Abmessungen durch zwei Offsets plus width und height definieren, anstatt alle vier Offsets zu verwenden. Das liegt daran, dass sich die padding/border-width nur nach innen vergrößern würden, wenn wir vier Offsets anstelle von zwei plus width und height verwenden würden.

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

Aus demselben Grund können wir auf unserem ::after-Pseudoelement nicht box-sizing: border-box haben.

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

Trotz dieser Einschränkungen kann diese Methode nützlich sein, wenn unser sich erweiterndes (Pseudo-)Element Textinhalt hat, den wir bei :hover nicht herumwandern sehen wollen, wie im folgenden Pen gezeigt, wo die ersten beiden Beispiele Offsets/Abmessungen ändern, während die letzten beiden Padding/Randbreiten ändern.

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

Ändern des Margins

Mit dieser Methode setzen wir zuerst die Offsets auf die :hover-Zustandswerte und ein margin, um dies zu kompensieren und uns die ursprüngliche Größenordnung zu geben.

.single-item::after {
  top: -1rem;
  right: -1rem;
  bottom: -3rem;
  left: -1rem;
  margin: 2rem 0 2rem 2rem;
}

Dann setzen wir dieses margin bei :hover auf Null.

.single-item:hover::after { margin: 0 }

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

Dies ist ein weiterer Ansatz, der für die CodePen-Situation gut funktioniert, obwohl mir keine anderen Anwendungsfälle einfallen. Beachten Sie auch, dass diese Methode, genau wie das Ändern von Offsets oder Abmessungen, die Größe der content-box beeinflusst, sodass jeder Textinhalt, den wir möglicherweise haben, verschoben und neu angeordnet wird.

Ändern der Schriftgröße

Dies ist wahrscheinlich die kniffligste aller Methoden und hat viele Einschränkungen, die wichtigste davon ist, dass wir keinen Textinhalt auf dem eigentlichen (Pseudo-)Element haben können, das sich erweitert/schrumpft - aber es ist eine weitere Methode, die im CodePen-Fall gut funktionieren würde.

Außerdem bewirkt font-size allein nichts, um eine Box größer oder kleiner zu machen. Wir müssen sie mit einer der zuvor diskutierten Eigenschaften kombinieren.

Zum Beispiel können wir die font-size für ::after auf 1rem einstellen, die Offsets auf den erweiterten Fall einstellen und em-Margins setzen, die dem Unterschied zwischen dem erweiterten und dem ursprünglichen Zustand entsprechen.

.single-item::after {
  top: -1rem;
  right: -1rem;
  bottom: -3rem;
  left: -1rem;
  margin: 2em 0 2em 2em;
  font-size: 1rem;
}

Dann, bei :hover, setzen wir die font-size auf 0.

.single-item:hover::after { font-size: 0 }

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

Wir können font-size auch mit Offsets verwenden, obwohl es etwas komplizierter wird.

.single-item::after {
  top: calc(2em - 1rem);
  right: -1rem;
  bottom: calc(2em - 3rem);
  left: calc(2em - 1rem);
  font-size: 1rem;
}

.single-item:hover::after { font-size: 0 }

Dennoch ist wichtig, dass es funktioniert, wie unten zu sehen ist:

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

Die Kombination von font-size mit Abmessungen ist noch haariger, da wir auch den vertikalen Offset-Wert bei :hover zusätzlich zu allem anderen ändern müssen.

.single-item::after {
  top: 1rem;
  right: -1rem;
  width: calc(100% + 2em);
  height: calc(100% + 4em);
  font-size: 0;
}

.single-item:hover::after {
  top: -1rem;
  font-size: 1rem
}

Nun ja, zumindest funktioniert es.

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

Dasselbe gilt für die Verwendung von font-size mit padding/border-width.

.single-item::after {
  top: 1rem;
  right: -1rem;
  width: 100%;
  height: 100%;
  font-size: 0;
}

.single-item:nth-child(1)::after {
  padding: 2em 0 2em 2em;
}

.single-item:nth-child(2)::after {
  border: solid 0 transparent;
  border-width: 2em 0 2em 2em;
}

.single-item:hover::after {
  top: -1rem;
  font-size: 1rem;
}

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

Ändern der Skalierung

Wenn Sie Artikel über animation-Performance gelesen haben, dann haben Sie wahrscheinlich gelesen, dass es besser ist, Transfoms zu animieren als Eigenschaften, die das Layout beeinflussen, wie Offsets, Margins, Ränder, Paddings, Abmessungen – ziemlich das, was wir bisher verwendet haben!

Das erste Problem, das hier auffällt, ist, dass das Skalieren eines Elements auch seine Eckabrundung skaliert, wie unten gezeigt:

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

Wir können dies umgehen, indem wir auch den border-radius in die entgegengesetzte Richtung skalieren.

Nehmen wir an, wir skalieren ein Element um den Faktor $fx entlang der x-Achse und um den Faktor $fy entlang der y-Achse und wir möchten seinen border-radius bei einem konstanten Wert $r halten.

Das bedeutet, wir müssen $r auch durch den entsprechenden Skalierungsfaktor entlang jeder Achse dividieren.

border-radius: #{$r/$fx}/ #{$r/$fy};
transform: scale($fx, $fy)

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

Beachten Sie jedoch, dass wir bei dieser Methode Skalierungsfaktoren verwenden müssen, nicht die Beträge, um die wir unser (Pseudo-)Element in die eine oder andere Richtung erweitern. Das Ermitteln der Skalierungsfaktoren aus den Abmessungen und Erweiterungsbeträgen ist möglich, aber *nur wenn* diese in Einheiten ausgedrückt sind, die eine bestimmte feste Beziehung zueinander haben. Während Präprozessoren Einheiten wie in oder px mischen können, da 1in immer 96px ist, können sie nicht auflösen, wie viel 1em oder 1% oder 1vmin oder 1ch in px ist, da ihnen der Kontext fehlt. Und calc() ist auch keine Lösung, da es uns nicht erlaubt, einen Längenwert durch einen anderen Längenwert zu teilen, um einen einheitenlosen Skalierungsfaktor zu erhalten.

Deshalb ist die Skalierung keine Lösung im CodePen-Fall, wo die ::after-Boxen Abmessungen haben, die vom Viewport abhängen, und sich gleichzeitig um feste rem-Beträge erweitern.

Aber wenn unser Skalierungsbetrag gegeben ist oder wir ihn leicht berechnen können, ist dies eine Option, die man in Betracht ziehen sollte, insbesondere da die Erstellung der Skalierungsfaktoren zu benutzerdefinierten Eigenschaften, die wir dann mit etwas Houdini-Magie animieren, unseren Code erheblich vereinfachen kann.

border-radius: calc(#{$r}/var(--fx))/ calc(#{$r}/var(--fy));
transform: scale(var(--fx), var(--fy))

Beachten Sie, dass Houdini nur in Chromium-Browsern mit dem Flag Experimental Web Platform features funktioniert.

Zum Beispiel können wir diese Kachelgitter-Animation erstellen:

Schleifende Kachelgitter-Animation (Demo, nur Chrome mit Flag)

Die quadratischen Kacheln haben eine Kantenlänge $l und eine Eckabrundung von $k*$l.

.tile {
  width: $l;
  height: $l;
  border-radius: calc(#{$r}/var(--fx))/ calc(#{$r}/var(--fy));
  transform: scale(var(--fx), var(--fy))
}

Wir registrieren unsere beiden benutzerdefinierten Eigenschaften:

CSS.registerProperty({
  name: '--fx', 
  syntax: '<number>', 
  initialValue: 1, 
  inherits: false
});

CSS.registerProperty({
  name: '--fy', 
  syntax: '<number>', 
  initialValue: 1, 
  inherits: false
});

Und wir können sie dann animieren:

.tile {
  /* same as before */
  animation: a $t infinite ease-in alternate;
  animation-name: fx, fy;
}

@keyframes fx {
  0%, 35% { --fx: 1 }
  50%, 100% { --fx: #{2*$k} }
}

@keyframes fy {
  0%, 35% { --fy: 1 }
  50%, 100% { --fy: #{2*$k} }
}

Schließlich fügen wir eine Verzögerung hinzu, die von den horizontalen (--i) und vertikalen (--j) Gitterindizes abhängt, um einen gestaffelten animation-Effekt zu erzeugen:

animation-delay: 
  calc((var(--i) + var(--m) - var(--j))*#{$t}/(2*var(--m)) - #{$t}), 
  calc((var(--i) + var(--m) - var(--j))*#{$t}/(2*var(--m)) - #{1.5*$t})

Ein weiteres Beispiel ist das folgende, bei dem die Punkte mit Hilfe von Pseudoelementen erstellt werden:

Schleifende Zacken-Animation (Demo, nur Chrome mit Flag)

Da Pseudoelemente zusammen mit ihren Eltern skaliert werden, müssen wir auch die Skalierungstransformation auf ihnen umkehren:

.spike {
  /* other spike styles */
  transform: var(--position) scalex(var(--fx));

  &::before, &::after {
    /* other pseudo styles */
    transform: scalex(calc(1/var(--fx)));
  }
}

Ändern von... clip-path?!

Dies ist eine Methode, die ich sehr mag, auch wenn sie den Support vor Chromium Edge und Internet Explorer ausschließt.

Fast jedes Nutzungsbeispiel von clip-path verwendet entweder einen polygon()-Wert oder einen SVG-Referenzwert. Wenn Sie jedoch einige meiner früheren Artikel gesehen haben, wissen Sie wahrscheinlich, dass es andere Grundformen gibt, die wir verwenden können, wie inset(), das wie unten gezeigt funktioniert:

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.
Funktionsweise der inset()-Funktion. (Demo)

Um den CodePen-Effekt mit dieser Methode zu reproduzieren, setzen wir die ::after-Offsets auf die erweiterten Zustandswerte und *schneiden* dann mit Hilfe von clip-path aus, was wir nicht sehen wollen.

.single-item::after {
  top: -1rem;
  right: -1rem;
  bottom: -3em;
  left: -1em;
  clip-path: inset(2rem 0 2rem 2rem)
}

Und dann, im :hover-Zustand, setzen wir alle Insets auf Null:

.single-item:hover::after {
  clip-path: inset(0)
}

Das kann man unten in Aktion sehen:

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

Okay, das funktioniert, aber wir brauchen auch eine Eckabrundung. Glücklicherweise erlaubt uns inset(), auch das anzugeben, als jeden gewünschten border-radius-Wert.

Hier reicht ein Wert von 10px für alle Ecken in beiden Richtungen.

.single-item::after {
  /* same styles as before */
  clip-path: inset(2rem 0 2rem 2rem round 10px)
}

.single-item:hover::after {
  clip-path: inset(0 round 10px)
}

Und das gibt uns genau das, was wir wollten:

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

Außerdem bricht es in Browsern, die dies nicht unterstützen, nichts, es bleibt einfach immer im erweiterten Zustand.

Diese Methode eignet sich zwar hervorragend für viele Situationen – einschließlich des CodePen-Anwendungsfalls –, aber sie funktioniert nicht, wenn unsere sich erweiternden/schrumpfenden Elemente Nachfahren haben, die außerhalb der border-box ihres geclippten Elternteils liegen, wie im letzten Beispiel mit der zuvor diskutierten Skalierungsmethode.