Restricting a (pseudo) element to its parent’s border-box

Avatar of Ana Tudor
Ana Tudor am

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

Haben Sie jemals gewollt, dass nichts von einem (Pseudo-)Element außerhalb der border-box seines Elternelements angezeigt wird? Falls Sie Schwierigkeiten haben, sich vorzustellen, wie das aussieht, nehmen wir an, wir wollten das folgende Ergebnis mit minimaler Markup und unter Vermeidung von spröder CSS erzielen.

Screenshot of the result we want to get, highlighting the fact that even though an element has both a padding and a border, its descendant gets clipped to the limit of its border.
Das gewünschte Ergebnis.

Das bedeutet, wir können keine Elemente nur zu visuellen Zwecken hinzufügen und keine Formen aus mehreren Teilen erstellen, weder direkt noch über Masken. Wir wollen auch lange, lange Listen von irgendetwas (denken Sie an Dutzende von background-Ebenen oder Box-Schatten oder Punkte innerhalb einer polygon()-Funktion) in unserem generierten Code vermeiden, denn obwohl die Ergebnisse Spaß machen können, ist es nicht wirklich praktikabel, so etwas zu tun!

Wie denken Sie, können wir das angesichts der Teile, auf die die Pfeile zeigen, erreichen? Wollen Sie es versuchen, bevor Sie meine Lösung unten prüfen? Es ist eines dieser Dinge, die auf den ersten Blick einfach erscheinen, aber wenn man es tatsächlich versucht, entdeckt man, dass es viel schwieriger ist.

Markup

Jedes Element ist ein Absatz-Element (<p>). Ich war faul und habe sie mit Pug aus einem Array von Objekten generiert, die die Gradientenstopp-Liste des Elements und seinen Absatztext enthalten.

- var data = [
-   {
-     slist: ['#ebac79', '#d65b56'], 
-     ptext: 'Pancake muffin chocolate syrup brownie.'
-   }, 
-   {
-     slist: ['#90cbb7', '#2fb1a9'], 
-     ptext: 'Cake lemon berry muffin plum macaron.'
-   }, 
-   {
-     slist: ['#8a7876', '#32201c'], 
-     ptext: 'Wafer apple tart pie muffin gingerbread.'
-   }, 
-   {
-     slist: ['#a6c869', '#37a65a'], 
-     ptext: 'Liquorice plum topping chocolate lemon.'
-   }
- ].reverse();
- var n = data.length;

while n--
  p(style=`--slist: ${data[n].slist}`) #{data[n].ptext}

Dies generiert folgendes unspektakuläres HTML

<p style='--slist: #ebac79, #d65b56'>Pancake muffin chocolate syrup brownie.</p>
<p style='--slist: #90cbb7, #2fb1a9'>Cake lemon berry muffin plum macaron.</p>
<p style='--slist: #8a7876, #32201c'>Wafer apple tart pie muffin gingerbread.</p>
<p style='--slist: #a6c869, #37a65a'>Liquorice plum topping chocolate lemon.</p>

Grundlegende Stile

Für die Absatz-Elemente setzen wir eine font, Dimensionen und einen border-radius, der die Hälfte des height-Wertes beträgt.

$w: 26em;
$h: 5em;

p {
  width: $w; height: $h;
  border-radius: .5*$h;
  background: silver;
  font: 1.5em/ 1.375 trebuchet ms, verdana, sans-serif;
}

Wir haben auch einen Dummy-background gesetzt, damit wir seine Grenzen sehen können.

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

Absatzhintergrund

Wir haben drei von oben nach unten verlaufende Gradienten, was bedeutet, dass wir jeden von ihnen innerhalb der Grenzen einer anderen Layout-Box platzieren können: Die obere Gradientenschicht ist auf die content-box beschränkt, die mittlere auf die padding-box und die untere auf die border-box. Wenn Sie eine detaillierte Auffrischung dieser Technik benötigen, schauen Sie sich diesen Artikel an, aber die Grundidee ist, dass Sie sich diese Layout-Boxen als verschachtelte Rechtecke vorstellen.

Illustration showing the layout boxes. The outermost box is the border-box. Inside it, a border-width away from the border limit, we have the padding-box. And finally, inside the padding-box, a padding away from the padding limit, we have the content-box.
Die Layout-Boxen. (Demo)

So ungefähr präsentieren die Browser-Entwicklertools sie.

Screenshot collage showing the graphical representation of the layout boxes in browsers' DevTools.
Die Layout-Boxen, wie sie von Chrome (links) vs. Firefox (Mitte) vs. Edge (rechts) angezeigt werden.

Sie fragen sich vielleicht, warum wir keine Gradienten mit unterschiedlichen Größen, die durch ihre background-size gegeben sind und background-repeat: no-repeat haben, schichten. Nun, das liegt daran, dass wir auf diese Weise nur Rechtecke ohne abgerundete Ecken erhalten.

Mit der background-clip-Methode, wenn wir einen border-radius haben, folgen unsere background-Schichten diesem. In der Zwischenzeit wird der tatsächlich gesetzte border-radius verwendet, um die Ecken der border-box abzurunden; derselbe Radius abzüglich der border-width rundet die Ecken der padding-box ab. Dann ziehen wir auch die padding ab, um die Ecken der content-box abzurunden.

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

Also, legen wir los!

Wir setzen einen transparenten border und eine padding. Wir stellen sicher, dass sie von den gesetzten Abmessungen abgezogen werden, indem wir zu box-sizing: border-box wechseln. Schließlich schichten wir drei Gradienten: den oberen, der auf die content-box beschränkt ist, den mittleren auf die padding-box und den unteren auf die border-box.

p {
  /* same styles as before */
  display: flex;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  border: solid $b transparent;
  padding: $p;
  background: 
    linear-gradient(#dbdbdb, #fff) content-box, 
    linear-gradient(var(--slist)) padding-box, 
    linear-gradient(#fff, #dcdcdc) border-box;
  text-indent: 1em;
}

Wir haben auch ein flex-Layout und einen text-indent gesetzt, um den Textinhalt von den Rändern des Banners wegzubewegen.

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

Nummerierung

Bevor wir zum kniffligen Teil kommen, kümmern wir uns um die Absatznummern!

Wir fügen sie mit einem counter hinzu, den wir als content-Wert auf dem :after-Pseudo-Element setzen. Zuerst machen wir dieses :after zu einem Quadrat, dessen Kantenlänge der height des Absatzes (was $h ist) abzüglich der oberen und unteren border-width (beide gleich $b) entspricht. Dann verwandeln wir dieses Quadrat in einen Kreis, indem wir border-radius: 50% darauf setzen. Wir lassen es das box-sizing und den border seines Elternelements inheriten und setzen dann seinen background auf ähnliche Weise, wie wir es für sein Elternelement getan haben.

$d: $h - 2*$b;

p {
  /* same styles as before */
  counter-increment: c;

  &:after {
    box-sizing: inherit;
    border: inherit;
    width: $d; height: $d;
    border-radius: 50%;
    box-shadow: 
      inset 0 0 1px 1px #efefef, 
      inset 0 #{-$b} rgba(#000, .1);
    background: 
      linear-gradient(var(--slist)) padding-box, 
      linear-gradient(#d0d0d0, #e7e7e7) border-box;
    color: #fff;
    content: counter(c, decimal-leading-zero);
  }
}

Na gut, das beginnt wie etwas auszusehen!

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

Wir müssen noch ein paar Anpassungen am CSS dieses :after-Pseudo-Elements vornehmen – einen margin-right, der minus der Polsterung des Elternelements ist, und Anpassungen an seinem inneren Layout, damit die Nummer genau in der Mitte liegt. Das ist so gut wie alles für den Nummerierungsteil!

p {
  /* same styles as before */

  &:after {
    /* same styles as before */
    display: grid;
    place-content: center;
    margin-right: -$p;
    text-indent: 0;
  }
}

Wir kommen näher!

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

Der knifflige Teil

Und da sind wir endlich!

Wir beginnen mit dem :before-Pseudo-Element, positionieren es absolut auf der right-Seite und machen es zu einem square, dessen Kantenlänge der height seines Elternelements entspricht.

p {
  /* same styles as before */
  position: relative;
  outline: solid 2px orange;

  &:before {
    position: absolute;
    right: -$b;
    width: $h;
    height: $h;
    outline: solid 2px purple;
    content: '';
  }
}

Wir haben sowohl diesem Pseudo-Element als auch seinem Elternelement Dummy-Umrisse gegeben, damit wir die Ausrichtung überprüfen können.

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

Nun geben wir diesem :before einen Dummy-background, drehen es und geben ihm dann einen border-radius und einen schönen box-shadow.

p {
  /* same styles as before */

  &:before {
    /* same styles as before */
    border-radius: $b;
    transform: rotate(45deg);
    box-shadow: 0 0 7px rgba(#000, .2);
    background: linear-gradient(-45deg, orange, purple);
  }
}

Und wir erhalten das folgende Ergebnis!

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

Jetzt haben wir ein kleines Problem: Das :before-Pseudo-Element ist absolut positioniert und liegt nun über den :after-Pseudo-Elementen, die die Nummerierung enthalten! Wir können dies beheben, indem wir position: relative auf dem :after-Pseudo-Element setzen.

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

Hier beginnt es interessant zu werden!

Beschränkung des Hintergrundgradienten

Zuerst müssen wir die Stopppositionen des Gradienten unseres :before-Pseudo-Elements so setzen, dass sie mit den bottom- und top-Kanten des Elternelements übereinstimmen. Das liegt daran, dass wir einen bestimmten Hex-Wert entlang der top-Kante des Elternelements und einen bestimmten Hex-Wert entlang der bottom-Kante des Elternelements haben möchten.

Annotated illustration. Shows the parent paragraph and its rotated :before pseudo-element, the gradient direction and the stop lines at the positions we're looking for.
Die zu berechnenden Stopppositionen.

Da wir unser quadratisches :before um 45° gedreht haben, zeigt seine obere linke Ecke nun nach oben (und umgekehrt zeigt seine untere rechte Ecke nach unten).

Animated .gif. Shows the square :before positioned on the right of its parent. Its top left corner and bottom right corner are highlighted as well as its vertical axis (vertical line passing through the intersection of its diagonals). Rotating our square by 45° means its top left corner now points up (and its bottom right corner points down).
Wie die Drehung die Position der Ecken des Quadrats verändert.

Ein Gradient zur oberen linken Ecke eines Quadrats ist ein Gradient in Richtung -45° (weil der Winkel von 0° bei 12 Uhr ist und die positive Richtung, wie bei Transformationen, die Uhrzeigersinnrichtung ist). Ein Gradient zu einer Ecke bedeutet, dass der 100%-Punkt sich in dieser Ecke befindet.

Animated .gif. Shows the square :before positioned on the right of its parent. Shows the linear gradient to the top left corner. After the rotation, since the top left corner points up, the gradient direction also goes up.
Wie die Drehung die Gradientenrichtung verändert.

Die 50%-Linie eines Gradienten verläuft immer durch den Mittelpunkt (den Punkt, an dem sich die Diagonalen schneiden) der Gradientenbox.

Die Gradientenbox ist die Box, innerhalb derer wir den Gradienten malen, und deren Größe durch background-origin bestimmt wird, was standardmäßig die padding-box ist. Da wir keinen border oder keine padding für unser :before-Pseudo-Element haben, sind alle drei Boxen (content-box, padding-box und border-box) in ihrem Abstand zueinander gleich und im Verhältnis zur Gradientenbox.

In unserem Fall haben wir die folgenden Linien, die senkrecht zur Richtung der nach -45° zeigenden Gradientenlinie verlaufen.

Annotated illustration. Shows the parent paragraph and its rotated :before pseudo-element, the gradient direction and the stop lines at 0% and 100%, at the two positions we want to get and at 50%.
Ermittlung der relevanten Stopppositionen.
  • die 0%-Linie, die durch die untere rechte Ecke des :before verläuft.
  • die bottom-Kante des Absatz-Elternelements des Pseudo-Elements.
  • die 50%-Linie, die unser Quadrat diagonal in zwei gespiegelte rechtwinklige gleichschenklige Dreiecke teilt; aufgrund der Art und Weise, wie wir unseren Absatz und seine Pseudo-Elemente ausgerichtet haben, ist diese Linie auch eine Mittellinie für den Absatz selbst, die ihn in zwei Hälften teilt, von denen jede eine height hat, die der Hälfte der height des Absatzes ($h) entspricht.
  • die top-Kante des Absatz-Elternelements des Pseudo-Elements.
  • die 100%-Linie, die durch die obere linke Ecke des :before verläuft.

Das bedeutet, wir müssen den nach -45° zeigenden Gradienten auf unserem :before-Pseudo-Element zwischen calc(50% - #{.5*$h}) (entspricht der bottom-Kante des Absatzes) und calc(50% + #{.5*$h}) (entspricht der top-Kante des Absatzes) einschränken.

Sicherlich, das tut es!

linear-gradient(-45deg, orange calc(50% - #{.5*$h}), purple calc(50% + #{.5*$h}))

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

Das Hinzufügen eines scharfen Übergangs von und zu transparent an diesen Stopppositionen macht es viel deutlicher, dass sie die richtigen sind.

linear-gradient(-45deg, 
      transparent calc(50% - #{.5*$h}), orange 0, 
      purple calc(50% + #{.5*$h}), transparent 0)

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

Beschränkung des Pseudo-Elements selbst

Der nächste Schritt ist, zu verhindern, dass das :before-Pseudo-Element außerhalb der Grenzen seines Elternelements überläuft.

Das ist einfach, oder? Setzen Sie einfach overflow: hidden auf den Absatz!

Nun, machen wir das!

Das ist das Ergebnis, das wir erhalten.

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

Ups, das ist nicht das, was wir wollten!

Side by side comparison of what we have using overflow: hidden (:before gets clipped to its parent's padding-box) and what we actually want to get (:before gets clipped to its parent's border-box).
Was wir mit overflow: hidden erhalten haben (links) vs. was wir wollen (rechts).

Das Problem ist, dass overflow: hidden alles außerhalb der padding-box eines Elements abschneidet, aber wir wollen hier die Teile des :before-Pseudo-Elements abschneiden, die außerhalb der border-box liegen, die in unserem Fall größer ist als die padding-box, weil wir einen nicht-null border haben, den wir nicht aufgeben können (und das Problem lösen, indem wir die border-box gleich der padding-box machen), weil wir drei background-Schichten auf unserem Absatz benötigen: die obere, die die content-box abdeckt, die mittlere, die die padding-box abdeckt, und die untere, die die border-box abdeckt.

Die Lösung? Nun, wenn Sie einen Blick auf die Tags geworfen haben, haben Sie es wahrscheinlich schon erraten: Verwenden Sie stattdessen clip-path!

So ziemlich jeder Artikel und jede Demo, die clip-path verwendet, nutzt entweder eine SVG-Referenz oder die polygon()-Formfunktion, aber das sind nicht die einzigen Optionen, die wir haben!

Eine weitere mögliche Formfunktion (und die, die wir hier verwenden werden) ist inset(). Diese Funktion definiert ein Ausschnittrechteck, das durch die Abstände von den top-, right-, bottom- und left-Kanten definiert wird. Kanten wovon? Nun, standardmäßig1 sind das die Kanten der border-box, was genau das ist, was wir hier brauchen!

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.
So funktioniert die inset()-Funktion. (Demo)

Also, weg mit overflow: hidden und stattdessen clip-path: inset(0) verwenden. Das ist das Ergebnis, das wir erhalten.

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

Das ist besser, aber noch nicht ganz das, was wir wollen, weil es den border-radius des Absatzes nicht berücksichtigt. Glücklicherweise erlaubt inset() auch die Angabe einer Abrundung, die jeden gewünschten border-radius-Wert annehmen kann. Kein Scherz, *jeder* gültige border-radius-Wert funktioniert – zum Beispiel dies.

clip-path: inset(0 round 15% 75px 35vh 13vw/ 3em 5rem 29vmin 12.5vmax)

Wir brauchen aber nur etwas viel Einfacheres.

$r: .5*$h;

p {
  /* same styles as before */
  border-radius: $r;
  clip-path: inset(0 round $r)
}

Und jetzt erhalten wir endlich das gewünschte Ergebnis.

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

Letzte Handgriffe

Da wir keinen Lila-Orange-Gradienten auf dem :before wünschen, ersetzen wir diese durch die tatsächlichen Werte, die wir benötigen. Dann platzieren wir die Absätze in der Mitte, weil das besser aussieht. Schließlich geben wir unseren Absätzen einen Schatten, indem wir einen drop-shadow() auf dem body setzen (wir können box-shadow nicht auf den Absätzen selbst verwenden, weil wir clip-path verwendet haben, das den box-shadow abschneidet, so dass wir ihn sowieso nicht sehen würden). Und das ist es!

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


  1. Wir *sollten* diesen <geometry-box>-Wert ändern können, aber Chrome implementiert diesen Teil des Standards nicht. Es gibt ein Issue dazu, das Sie als Favorit markieren oder kommentieren können, um Ihre Anwendungsfälle für die Änderung des Standardwerts darzulegen.