Using Pseudo-Elements for Box Shadows
What if I wanted a box-shadow
with a linear gradient? I can add a color to the box-shadow
property, but I can't define a linear gradient on it. A would work, but not B:
A:
{
/* offset-x | offset-y | blur-radius | spread-radius | color */
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.2);
}
B:
{
/* offset-x | offset-y | blur-radius | spread-radius | color */
box-shadow: 2px 2px 2px 1px linear-gradient(-45deg, #687bdd 0%, #221e22 50%, #e90e45 100%);
}
That's because the linear-gradient
property is actually a special type of image, not a color. Since a shadow cannot have an image, we have a problem.
There's also the additional issue of the shape of the shadow. Box shadows are boxes by default. We could use drop-shadow
if we wanted, but then we wouldn't be able to define a linear gradient on that. Moreover, drop-shadows conform to the shape of the object they are shadowing. You don't get to define a custom shape on that shadow.
To solve this problem, we can use CSS pseudo-elements. In particular, I'm talking about the ::before
and ::after
pseudo-elements.
CSS pseudo-elements allow you to create extra content for your page without actually having to add elements to the HTML. There are A LOT of them, but ::before
and ::after
are the most used by far. In this case, we'll use these to make a custom shadow for our box with some cool effects.
Before we start, below are some important rules we need to remember about working with ::before
and ::after
:
They MUST have a
content
property defined on them. You may make this an empty string, which is the convention, but you cannot omit it from the pseudo-element's CSS rule or it won't show up on your page.A pseudo-element is actually a child of the element it is defined on. This explains the monikers
::before
and::after
. One appears before the rest of the containing element's children; the other appears after them.Most of the rules that apply to normal elements apply to pseudo-elements as well. You can position and style them, and they will typically inherit styles from their parents like good little children.
And now that we've set the ground rules, let's get to work!
A Gradient Shadow
First, we define a simple card. Inside it, I'll add some text telling us what we're doing because why not?!
<div class="card">
<h2 class="gradient-shadow">Gradient Shadow</h2>
</div>
That's all we need. We can now style it. We'll start by defining a height and width for the card. We'll also turn the card into a flex
container and center everything so it looks nice. We'll position: relative
it so that we can position: absolute
its children and play around with them more easily. Finally, we'll give it a white background for simplicity.
.card {
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 300px;
width: 300px;
background: white;
}
And now comes the fun part: adding the shadow. For this, we'll use a ::before
pseudo-element, though we can just as easily use an ::after
element. Since we'll position: absolute
it, we are not constrained by where it logically appears in its parent element.
Here's a short list of what we'll do with the ::before
element:
- We'll give it an empty string value for the
content
property. - We'll
position: absolute
it. This allows us to define its offsets, and also whether it appears behind or in front of everything else. - Its
top
offset will be40px
and itsleft
offset will be 0. Since it's a shadow, we want it slightly below the parent container (by 'slightly', I mean40px
below the parent). - We want the pseudo-element to be just as large as its parent. That way, it will be visible. We will therefore set the
width
andheight
properties to100%
.
Note: elements are automatically assigned
display: block
when youposition: absolute
them, so we can set a width and height on our::before
pseudo-element without explicitly assigning a display property to it. Otherwise,::before
and::after
pseudo-elements havedisplay: inline
by default.
We'll give our
::before
pseudo-element alinear-gradient
for its background. For this, I just used a gradient generator and picked something that looked good.To make our
::before
pseudo-element more shadow-like, we'll give it a blur. For this, we set itsfilter
property to, well,blur
. You can specify any blur size you want. I chose30px
because it looked more natural to me.- The pseudo-element is still sitting in front of its parent. We'll set its
z-index
to-1
so it goes behind it. - Finally, I also decided to
transform: scale()
the pseudo-element down slightly. This will mean we see less of it bulging out of the sides of its parent. Since it's been shifted downwards, we'll see most of the 'shadow' on the bottom of the container.
The final code should look something like below:
.card::before {
content: "";
position: absolute;
top: 40px;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(43deg, #4158D0 0%, #C850C0 46%, #FFCC70 100%);
filter: blur(30px);
z-index: -1;
transform: scale(0.9);
}
Below is a pen of the final result. Note I added some styling for the body so everything looks good.
And we have a nice rainbow shadow!
But why stop there? Let's try and make something slightly fancier.
Making A Skewed Shadow
Remember the ground rules we set? We said that you can do just about anything with a pseudo-element that you can do with a regular DOM element. That is an immense amount of power! Linear gradients are just the tip of the iceberg. You can shape them however you like and even go so far as to animate them. To give you a taste of this power, we'll create a skewed shadow with 2 different colors on opposite corners.
We'll start by writing the HTML. It will be the same div as before, except I won't be adding any text to it in this case. Nothing too complicated:
<div class="card">
<div>
Let's style the card:
- As usual, we'll
position: relative
our card so we have control over the positioning of its children. - We'll set the
width
andheight
properties for our card. I chose the dimensions220px
and320px
respectively because I thought it looked good. - Finally, I gave the card a background color. This was so that it would look nice next to its colored shadow later on. This part is at your discretion, really.
The final code should be as below:
.card {
position: relative;
width: 220px;
height: 320px;
background: #030a2e;
}
Now we can style the shadow.
For this, the concept is to have a shadow that juts out of 2 opposite corners of the card, with a distinct color at each corner. So, for example, if the shadow is to poke out of the top left and bottom right corners, it would have one color in the top left corner and another in the bottom right. To help it stand out, it would look slightly skewed at the corners.
- We can achieve the effect of 2 colors by defining a linear gradient on the shadow. It would start at one color in one corner and transition into another one by the time it reached the other corner.
- We'll then place the shadow behind the card using its
z-index
property so all we can see are the colors popping out of the corners. - To make the corners actually pop out, we'll use the
transform: skewX() skewY()
property. - The other stuff, like positioning and setting the
content
property will be like in the previous example.
There's something a little more complicated to talk about here (but only a little):
In the previous example, we explicitly set the width and height of our pseudo-element. Before that, we had given it a vertical (using top
) and horizontal (using left
) offset. We could do this because we had set its position
property to absolute
.
Elements with position: absolute
have an interesting property that lets us set their width and height in other ways: if we set all 4 offsets (top
, right
, bottom
and left
), we can stretch the element. For example, if we set all 4 properties above to have a value of 4px
, the pseudo-element will have8px
shorter width
and height
properties (4px
less on top, 4px less on the right, etc.) than its parent. It will be as if we had set the width
and height
properties explicitly.
On the other hand, if we set all the offsets to something like -4px
, the pseudo-element would have 8px
longer width
and height
properties. So common is this practice of uniformly setting the offsets to size a position: absolute
element that there is a CSS property to do it all at once: inset
.
In this case, I want my ::before
element to be 8px
wider and taller than its container. That way, it will be easy to skew it at the corners. 8px
wider and taller is the same as left: -4px
, right: -4px
, top: -4px
, and bottom: -4px
. We can achieve that in one fell swoop with inset: -4px
, allowing us to write less code.
Note: the
inset
property takes a single value, so it should only be used where all 4 offsets would have the same value.
The final code for the before element will look something like what we have below:
.card::before {
content: '';
position: absolute;
inset: -4px;
transform: skewX(3deg) skewY(6deg);
background: linear-gradient(-45deg, #687bdd 0%, #221e22 50%, #e90e45 100%);
z-index: -1;
}
I also added some rules for the body, just so everything is well centered, and gave it the same background color as the card, because I thought it made the effect look even nicer. The shadow should appear as below:
Afterglow
The above card looks nice, but we could make it look even more interesting without adding a single element to the HTML. In this case, we'll leverage the ::after
pseudo-element.
The idea is simple: We'll give the ::after
pseudo-element all the properties we gave the ::before
pseudo-element, and then some. We'll add a blur effect on the ::after
element using the filter
property so it simulates a glow:
.card::after {
content: '';
position: absolute;
inset: -4px;
transform: skewX(3deg) skewY(6deg);
background: linear-gradient(-45deg, #687bdd 0%, #221e22 50%, #e90e45 100%);
filter: blur(50px);
z-index: -1;
}
And now I present to you the end result!
As you can see, we've been able to achieve a lot with just a single HTML element, while doing most of the work in CSS, all thanks to the power of pseudo-elements.
Caveats
There are some things to consider, and gotchas to avoid, while we use pseudo-elements in our daily work.
Accessibility
Pseudo-elements don't appear in the DOM. That's probably why they're called 'pseudo-elements'; they're not actual HTML elements. You should therefore avoid using them to represent important information on your page as that might lead to accessibility issues. Not all screen readers can detect pseudo-elements.
Z-index
In this exercise, we played around with the z-index
of elements on the page to achieve the shadow effect. Note that the z-index
property can only be applied to positioned elements. Also, whenever you define z-index
on a suitable element, you create a new stacking context for that element and its children. If you ever find z-index
not working the way you expect it to, try to figure out whether z-index
was defined on an element earlier in the CSS, and whether this is determining where your current element is in the stacking context. You can read the article I've linked on stacking contexts to understand how this works.
Conclusion
And with that, we're done! We managed to create colorful shadows using pseudo-elements. We've only touched the tip of the iceberg here. As you can imagine, a lot more is possible with pseudo-elements. There are a lot of talented people out there who come up with new ways to utilize this CSS feature every day, sometimes making artwork that would give Picasso or Van Gogh a run for their money. Hopefully, this introduction will inspire you to create your own art too.
Attributions
I was inspired to write this by this video, where I learned this technique. It's a great channel with lots of gems. Be sure to check it out!
Any interesting thoughts? Let's talk in the comments!