Creating Shadows with Pseudo-Elements

Creating Shadows with Pseudo-Elements

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 be 40px and its left offset will be 0. Since it's a shadow, we want it slightly below the parent container (by 'slightly', I mean 40px 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 and height properties to 100%.

Note: elements are automatically assigned display: block when you position: 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 have display: 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 its filter property to, well, blur. You can specify any blur size you want. I chose 30px 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 and height properties for our card. I chose the dimensions 220px and 320px 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!