Have you ever wanted to ensure that nothing of a (pseudo) element gets displayed outside its parent’s border-box
? In case you’re having trouble picturing what that looks like, let’s say we wanted to get the following result with minimal markup and avoiding brittle CSS.
![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.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/desired_result-arrows.png?ssl=1)
This means we cannot add any elements just for visual purposes and we cannot create shapes out of multiple pieces, whether that’s directly or via masks. We also want to avoid long, long lists of anything (think something like tens of background
layers or box shadows or points inside a polygon()
function) in our generated code because, while the results can be fun, it’s not really practical to do something like that!
How do you think we can achieve this, given the parts the arrows point towards? Fancy giving it a try before checking my solution below? It’s one of those things that seems simple at first, but once you actually try it, you discover it’s much trickier.
Markup
Each item is a paragraph (<p>
) element. I was lazy and generated them with Pug out of an array of objects which hold the item’s gradient stop list and its paragraph text:
- 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}
This generates the following unspectacular 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>
Basic styles
For the paragraph elements, we set a font
, dimensions and a border-radius
that’s half the height
value:
$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;
}
We’ve also set a dummy background
so we can see their limits:
See the Pen by thebabydino (@thebabydino) on CodePen.
Paragraph background
We have three top-to-bottom gradients, which means we can place each of them within the limits of a different layout box: the top gradient layer is limited to the content-box
, the middle one to the padding-box
and the bottom one to the border-box
. If you need an in-depth refresher on this technique, check out this article, but the basic idea is you picture these layout boxes as nested rectangles.
![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.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/layout_boxes.png?ssl=1)
This is pretty much how browser DevTools presents them.
![Screenshot collage showing the graphical representation of the layout boxes in browsers' DevTools.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/dev_tools.png?ssl=1)
You may be wondering why we wouldn’t layer gradients with different sizes given by their background-size
and that have background-repeat: no-repeat
. Well, this is because we only get rectangles without rounded corners this way.
Using the background-clip
method, if we have a border-radius
, our background
layers will follow that. Meanwhile, the actual border-radius
we set is being used to round the corners of the border-box
; that same radius minus the border-width
rounding the corners of the padding-box
. Then we’re subtracting the padding
as well to round the corners of the content-box
.
See the Pen by thebabydino (@thebabydino) on CodePen.
So let’s code this!
We set a transparent
border
and a padding
. We make sure they get subtracted from the dimensions we’ve set by switching to box-sizing: border-box
. Finally, we layer three gradients: the top one restricted to the content-box
, the middle one to the padding-box
and the bottom one to the 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;
}
We’ve also set a flex
layout and a text-indent
to move the text content away from the banner edges:
See the Pen by thebabydino (@thebabydino) on CodePen.
Numbering
Before we move on to the tricky part, let’s get the paragraph numbers out of the way!
We add them using a counter
we set as the content
value on the :after
pseudo-element. We first make this :after
a square whose edge length equals the paragraph height
(which is $h
) minus the top and bottom border-width
(both equal to $b
). Then we turn this square into a circle by setting border-radius: 50%
on it. We make it inherit
its parent’s box-sizing
and border
and we then set its background
in a similar manner as we did for its parent.
$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);
}
}
Alright, this is starting to look like something!
See the Pen by thebabydino (@thebabydino) on CodePen.
We still need to make a few tweaks to the CSS of this :after
pseudo-element — a margin-right
that’s minus its parent’s padding and adjustments to its inner layout so we have the number dead in the middle. That’s pretty much it for the numbering part!
p {
/* same styles as before */
&:after {
/* same styles as before */
display: grid;
place-content: center;
margin-right: -$p;
text-indent: 0;
}
}
We’re getting closer!
See the Pen by thebabydino (@thebabydino) on CodePen.
The tricky part
And we’re finally here!
We start off by using the :before
pseudo-element, absolutely positioning it on the right
side and making it a square
whose edge length equals its parent’s height
:
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: '';
}
}
We’ve also given both this pseudo-element and its parent some dummy outlines so that we can check the alignment:
See the Pen by thebabydino (@thebabydino) on CodePen.
Alright, we now give this :before
a dummy background
, rotate it, and then give it a border-radius
and a nice 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);
}
}
And we get the following result!
See the Pen by thebabydino (@thebabydino) on CodePen.
We now have a small problem: the :before
pseudo-element is absolutely positioned and is now on top of the :after
pseudo-elements that holds the numbering! We can fix this by setting position: relative
on the :after
pseudo-element.
See the Pen by thebabydino (@thebabydino) on CodePen.
Here’s where things start getting interesting!
Restricting the background gradient
First, we need to set the stop positions on the gradient of our :before
pseudo-element such that they match the bottom
and top
edges of the parent. This is because we want to have a certain hex value along the top
edge of the parent and a certain hex value along the bottom
edge of the parent.
![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.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/lines_for_stops.png?ssl=1)
Since we’ve rotated our square :before
by 45°
, its top-left corner now points upward (and, conversely, its bottom-right corner points downward).
![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).](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/move_corner.gif?ssl=1)
A gradient to the top-left corner of a square is a gradient in the -45°
direction (because the 0°
angle is at 12 o’clock and the positive direction is, just as for transforms, the clockwise one). A gradient to a corner means the 100%
point is located in that corner)
![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.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/topleft_dir.gif?ssl=1)
The 50%
line of a gradient always passes through the midpoint (the point at the intersection of the diagonals) of the gradient box.
The gradient box is the box within which we paint the gradient and whose size is given by the background-size
. Since we haven’t set a background-size
, the default for gradients is to use the entire box defined by background-origin
, which is the padding-box
by default. Since we don’t have a border
or a padding
on our :before
pseudo-element, it results that all three boxes (content-box
, padding-box
and border-box
) are equal in space between them and equal in proportion to the gradient box.
In our case, we have the following lines perpendicular to the direction of the -45°
-pointing gradient line:
![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%.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/perc_lines.png?ssl=1)
- the
0%
line, passing through the bottom right corner of the:before
- the
bottom
edge of the pseudo-element’s paragraph parent - the
50%
line which splits our square diagonally into two mirrored right isosceles triangles; given the way we’ve aligned our paragraph and its pseudo-elements, this line is also a midline for the paragraph itself, splitting it into two halves, each with aheight
equal to half the paragraph’sheight
($h
). - the
top
edge of the pseudo-element’s paragraph parent - the
100%
line, passing through the top left corner of the:before
This means we need to restrict the -45°
-pointing gradient on our :before
pseudo-element between calc(50% - #{.5*$h})
(corresponding to the paragraph’s bottom
edge) and calc(50% + #{.5*$h})
(corresponding to the paragraph’s top
edge).
Sure enough, this does it!
linear-gradient(-45deg, orange calc(50% - #{.5*$h}), purple calc(50% + #{.5*$h}))
See the Pen by thebabydino (@thebabydino) on CodePen.
Adding a sharp transition from and to transparent
at these stop positions makes it a lot more obvious they’re the correct ones:
linear-gradient(-45deg,
transparent calc(50% - #{.5*$h}), orange 0,
purple calc(50% + #{.5*$h}), transparent 0)
See the Pen by thebabydino (@thebabydino) on CodePen.
Restricting the pseudo-element itself
The next step is to prevent the :before
pseudo-element from spilling outside the boundaries of its parent.
That’s easy, right? Just set overflow: hidden
on the paragraph!
Well, let’s do that!
This is the result we get:
See the Pen by thebabydino (@thebabydino) on CodePen.
Oops, this is not what we were going for!
![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).](https://i0.wp.com/css-tricks.com/wp-content/uploads/2019/05/outline_issue.png?ssl=1)
overflow: hidden
(left) vs. what we want (right).The problem is overflow: hidden
cuts out everything outside an element’s padding-box
, but what we want here is to cut the parts of the :before
pseudo-element that are outside the border-box
, which is larger than the padding-box
in our case because we have a non-zero border
we cannot ditch (and solve the issue by making the border-box
equal to the padding-box
) because we need three background
layers on our paragraph: the top one covering the content-box
, the middle one covering the padding-box
and the bottom one covering the border-box
.
The solution? Well, if you’ve taken a peek at the tags, you’ve probably guessed by now: use clip-path
instead!
Pretty much every article and demo using clip-path
out there makes use of either an SVG reference or the polygon()
shape function, but these are not the only options we have!
Another possible shape function (and the one we’ll be using here) is inset()
. This function specifies a clipping rectangle defined by the distances from the top
, right
, bottom
and left
edges. Edges of what? Well, by default1, that’s the edges of the border-box
, which is exactly what we need here!
inset()
function works. (Demo)So let’s ditch overflow: hidden
and use clip-path: inset(0)
instead. This is the result we get:
See the Pen by thebabydino (@thebabydino) on CodePen.
This is better, but not quite what we want because it doesn’t take into account the paragraph’s border-radius
. Fortunately, inset()
also lets us specify a rounding that can take any border-radius
value we want. No joke, any valid border-radius
value works — for example, this:
clip-path: inset(0 round 15% 75px 35vh 13vw/ 3em 5rem 29vmin 12.5vmax)
We only need something much simpler though:
$r: .5*$h;
p {
/* same styles as before */
border-radius: $r;
clip-path: inset(0 round $r)
}
And now we finally get the result we wanted:
See the Pen by thebabydino (@thebabydino) on CodePen.
Final touches
Since we don’t want a purple-orange gradient on the :before
, we replace those with the actual values we need. We then place the paragraphs in the middle because that looks better. Finally, we give our paragraphs a shadow by setting a drop-shadow()
on the body
(we cannot use box-shadow
on the paragraphs themselves because we’ve used clip-path
, which clips out the box-shadow
so we wouldn’t see it anyway). And that’s it!
See the Pen by thebabydino (@thebabydino) on CodePen.
So.many.ideas… thank you!!!
OMG Ana, this is some next level CSS! Amazing, and the tutorial steps and explanation is incredible. I’ll be sharing as a showcase for the whole front-end team
I’m reading this, and I’m wondering if you’ve any tutorials on CSS for Beginners. I need you to teach me stuff!
I know it’s not exactly the design you try to achieve, but instead of the
clip-path
instruction on thep
pseudo-element, if we simply apply 3border-radius
of50%
and 1 of only$r
, we get something extremely close and way easier to handle:p {
// -webkit-clip-path: inset(0 round $r);
// clip-path: inset(0 round $r);
&::before {
...
border-radius: 50% 50% 50% $b;
}
Example here
After revisiting this article again (I’m always amazed by the ingenuity of it), I’ve noticed just a small little error at the #numbering section, where you show:
counter-increment: $d
instead of the actually used
counter-increment: c
Oops! Fixed now, thanks for the heads-up!