Different Ways to Get CSS Gradient Shadows
It’s a question I hear asked quite often: Is it possible to create shadows from gradients instead of solid colors? There is no specific CSS property that does this (believe me, I’ve looked) and any blog post you find about it is basically a lot of CSS tricks to approximate a gradient. We’ll actually cover some of those as we go.
But first… another article about gradient shadows? Really?
Yes, this is yet another post on the topic, but it is different. Together, we’re going to push the limits to get a solution that covers something I haven’t seen anywhere else: transparency. Most of the tricks work if the element has a non-transparent background but what if we have a transparent background? We will explore this case here!
Let’s start with the solution that’ll work for 80% of most cases. The most typical case: you are using an element with a background, and you need to add a gradient shadow to it. No transparency issues to consider there.
Here’s a demo showing the gradient shadow next to a classic
If you look closely you will notice that both shadows are a little different, especially the blur part. It’s not a surprise because I am pretty sure the
filter property’s algorithm works differently than the one for
box-shadow. That’s not a big deal since the result is, in the end, quite similar.
The first solution that I recommend is to use a 3D
Instead of using
z-index: -1, we will use a negative translation along the Z-axis. We will put everything inside
translate3d(). Don’t forget to use
transform-style: preserve-3d on the main element; otherwise, the 3D
transform won’t take effect.
As far as I know, there is no side effect to this solution… but maybe you see one. If that’s the case, share it in the comment section, and let’s try to find a fix for it!
If for some reason you are unable to use a 3D
transform, the other solution is to rely on two pseudo-elements —
::after. One creates the gradient shadow, and the other reproduces the main background (and other styles you might need). That way, we can easily control the stacking order of both pseudo-elements.
As I said, this solution is probably good enough in a majority of cases where you want a gradient shadow, as long as you don’t need to support transparency. But we are here for the challenge and to push the limits, so even if you don’t need what is coming next, stay with me. You will probably learn new CSS tricks that you can use elsewhere.
Let’s pick up where we left off on the 3D
transform and remove the background from the main element. I will start with a shadow that has both offsets and spread distance equal to
Indeed, there’s no way to do that, but we can simulate it using a particular polygon pattern:
Tada! We have a gradient shadow that supports transparency. All we did is add a
clip-path to the previous code. Here is a figure to illustrate the polygon part.
The blue area is the visible part after applying the
clip-path. I am only using the blue color to illustrate the concept, but in reality, we will only see the shadow inside that area. As you can see, we have four points defined with a big value (
B). My big value is
100vmax, but it can be any big value you want. The idea is to ensure we have enough space for the shadow. We also have four points that are the corners of the pseudo-element.
The arrows illustrate the path that defines the polygon. We start from
(-B, -B) until we reach
(0,0). In total, we need 10 points. Not eight points because two points are repeated twice in the path (
There’s still one more thing left for us to do, and it’s to account for the spread distance and the offsets. The only reason the demo above works is because it is a particular case where the offsets and spread distance are equal to
Let’s define the spread and see what happens. Remember that we use
inset with a negative value to do this:
The pseudo-element is now bigger than the main element, so the
clip-path cuts more than we need it to. Remember, we always need to cut the part inside the main element (the area inside the green border of the example). We need to adjust the position of the four points inside of
We’ve defined a CSS variable,
--s, for the spread distance and updated the polygon points. I didn’t touch the points where I am using the big value. I only update the points that define the corners of the pseudo-element. I increase all the zero values by
--s and decrease the
100% values by
It’s the same logic with the offsets. When we translate the pseudo-element, the shadow is out of alignment, and we need to rectify the polygon again and move the points in the opposite direction.
There are two more variables for the offsets:
--y. We use them inside of
transform and we also update the
clip-path values. We still don’t touch the polygon points with big values, but we offset all the others — we reduce
--x from the X coordinates, and
--y from the Y coordinates.
Now all we have to do is to update a few variables to control the gradient shadow. And while we are at it, let’s also make the blur radius a variable as well:
Do we still need the 3D
It all depends on the border. Don’t forget that the reference for a pseudo-element is the padding box, so if you apply a border to your main element, you will have an overlap. You either keep the 3D
transform trick or update the
inset value to account for the border.
Here is the previous demo with an updated
inset value in place of the 3D
I‘d say this is a more suitable way to go because the spread distance will be more accurate, as it starts from the border-box instead of the padding-box. But you will need to adjust the
inset value according to the main element’s border. Sometimes, the border of the element is unknown and you have to use the previous solution.
If you try adding
border-radius to the element when using the non-transparent solution we started with, it is a fairly trivial task. All you need to do is to inherit the same value from the main element, and you are done.
Even if you don’t have a border radius, it’s a good idea to define
border-radius: inherit. That accounts for any potential
border-radius you might want to add later or a border radius that comes from somewhere else.
It’s a different story when dealing with the transparent solution. Unfortunately, it means finding another solution because
clip-path cannot deal with curvatures. That means we won’t be able to cut the area inside the main element.
I decided to insert an extra element for the sake of simpler code. Here’s the markup:
I am using a custom element,
<sh>, to avoid any potential conflict with external CSS. I could have used a
<div>, but since it’s a common element, it can easily be targeted by another CSS rule coming from somewhere else that can break our code.
The first step is to position the
<sh> element and purposely create an overflow:
The code may look a bit strange, but we’ll get to the logic behind it as we go. Next, we create the gradient shadow using a pseudo-element of
As you can see, the pseudo-element uses the same code as all the previous examples. The only difference is the 3D
transform defined on the
<sh> element instead of the pseudo-element. For the moment, we have a gradient shadow without the transparency feature:
Note that the area of the
<sh> element is defined with the black outline. Why I am doing this? Because that way, I am able to apply a
mask on it to hide the part inside the green area and keep the overflowing part where we need to see the shadow.
I know it’s a bit tricky, but unlike
mask property doesn’t account for the area outside an element to show and hide things. That’s why I was obligated to introduce the extra element — to simulate the “outside” area.
Also, note that I am using a combination of
inset to define that area. This allows me to keep the padding-box of that extra element the same as the main element so that the pseudo-element won’t need additional calculations.
Let’s isolate the
<sh> element to understand what is happening there:
Here’s what we get:
Note how the inner radius matches the main element’s
border-radius. I have defined a big border (
150px) and a
border-radius equal to the big border plus the main element’s radius. On the outside, I have a radius equal to
150px + R. On the inside, I have
150px + R - 150px = R.
We must hide the inner (blue) part and make sure the border (red) part is still visible. To do that, I’ve defined two mask layers —One that covers only the content-box area and another that covers the border-box area (the default value). Then I excluded one from another to reveal the border.
Are there any drawbacks to this method?
Yes, this definitely not perfect. The first issue you may face is related to using a border on the main element. This may create a small misalignment in the radii if you don’t account for it. We have this issue in our example, but perhaps you can hardly notice it.
The fix is relatively easy: Add the border’s width for the
The last drawback I am aware of is when you’re working with a complex
border-radius. For example, if you want a different radius applied to each corner, you must define a variable for each side. It’s not really a drawback, I suppose, but it can make your code a bit tougher to maintain.
We’ve reached the end! The magic behind gradient shadows is no longer a mystery. I tried to cover all the possibilities and any possible issues you might face. If I missed something or you discover any issue, please feel free to report it in the comment section, and I’ll check it out.
Again, a lot of this is likely overkill considering that the de facto solution will cover most of your use cases. Nevertheless, it’s good to know the “why” and “how” behind the trick, and how to overcome its limitations. Plus, we got good exercise playing with CSS clipping and masking.
If you need help creating a digital marketing strategy for your business, don’t hesitate to contact one of Digidude’s consultants.