Home >Web Front-end >CSS Tutorial >Different Ways to Get CSS Gradient Shadows

Different Ways to Get CSS Gradient Shadows

Lisa Kudrow
Lisa KudrowOriginal
2025-03-09 10:03:14432browse

Different Ways to Get CSS Gradient Shadows

I often ask a question: Can I create shadows with gradient colors instead of solid colors? There are no special properties in CSS to achieve this (believe me, I've looked for it), and any blog posts you find about this are basically some similar gradient CSS tricks. We'll cover some of these tips next.

But first...is another article about gradient shadows? Really?

Yes, this is another post on this topic, but it's different. Together, we will push the limits and find a solution that covers something I have never seen anywhere else: Transparency. Most tips work when elements have non-transparent backgrounds, but what if we have transparent backgrounds? We will discuss this situation here!

Before we get started, let me introduce my gradient shadow generator. You just need to adjust the configuration and you can get the code. But keep reading because I will help you understand all the logic behind generating the code.

Catalog

  • Non-transparent solution
  • Transparent Solution
  • Add rounded corners
  • Summary

Non-transparent solution

Let's start with solutions that work for 80% of common situations. The most typical case is that you are using an element with a background and you need to add a gradient shadow to it. There is no need to consider transparency here.

The solution is to rely on a pseudo-element that defines the gradient. You put it behind the actual element and apply a blur filter to it.

<code>.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* 控制扩散 */
  transform: translate(10px, 8px); /* 控制偏移量 */
  z-index: -1; /* 将元素置于后面 */
  background: /* 你的渐变色在这里 */;
  filter: blur(10px); /* 控制模糊 */
}</code>

The code looks a lot, because it does have a lot. Here's how to use box-shadow instead of a gradient if we use solid colors instead of gradients.

<code>box-shadow: 10px 8px 10px 5px orange;</code>

This should give you a good idea of ​​what the value in the first piece of code does. We have X and Y offsets, fuzzy radius, and diffusion distance. Note that we need a negative diffusion distance value from the inset property.

This is a demo showing gradient shadows side by side with classic box-shadow:

If you look closely, you will notice that the two shadows are slightly different, especially the blurred parts. This is not surprising, because I'm pretty sure the algorithm for filter attributes is different from that of box-shadow. It's no big deal, because the end result is very similar.

This solution is good, but still has some disadvantages related to the z-index:-1 declaration. Yes, there is a "stack context" happening there!

I applied a transformation to the main element, and then the shadow is no longer below the element. This is not a mistake, but a logical result of stacking contexts. Don't worry, I won't start giving boring explanations of stacking contexts (I've done this in Stack Overflow threads), but I'll still show you how to fix it.

The first solution I recommend is to use 3D conversion:

<code>.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* 控制扩散 */
  transform: translate(10px, 8px); /* 控制偏移量 */
  z-index: -1; /* 将元素置于后面 */
  background: /* 你的渐变色在这里 */;
  filter: blur(10px); /* 控制模糊 */
}</code>

We do not use z-index: -1, but use a negative translation along the Z axis. We put everything in translate3d(). Don't forget to use transform-style: preserve-3d on the main element; otherwise, the 3D conversion will not take effect.

As far as I know, this solution has no side effects...but maybe you will see one. If so, please share in the comment section and let's try to find a solution!

If for some reason you can't use 3D conversion, another solution is to rely on two pseudo-elements—::before and::after. One creates a gradient shadow, the other copys the main background (and other styles you might need). This way, we can easily control the stacking order of two pseudo-elements.

<code>box-shadow: 10px 8px 10px 5px orange;</code>

It is important to note that we are forcing the to create a stacking context by declaring z-index: 0 on it or any other attribute that does the same. Also, don't forget that the pseudo-element treats the fill box of the main element as a reference. Therefore, if the main element has borders, this needs to be considered when defining the pseudo-element style. You will notice that I use inset: -2px on ::after to consider borders defined on main elements.

As I said, this solution may be good enough in most cases as long as you don't need to support transparency, you want a gradient shadow. But we came to challenge and push the limits, so even if you don’t need what you want to talk about next, please keep paying attention. You may learn new CSS tips that you can use elsewhere.

Transparent Solution

Let's start where we end in 3D transformation and remove the background from the main element. I will start with a shadow where both the offset and diffusion distance are equal to 0.

The idea is to find a way to crop or hide everything inside an area of ​​the element (within a green border) while retaining the external content. We will use clip-path for this. But you might be wondering how clip-path can be cropped inside an element

. Indeed, there is no way to do this, but we can simulate it with a specific polygon pattern:

Look! We get a gradient shadow that supports transparency. All we did was add a clip-path to the previous code. Here is a diagram to illustrate the polygon part.
<code>.box {
  position: relative;
  transform-style: preserve-3d;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px;
  transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
  background: /* .. */;
  filter: blur(10px);
}</code>

The blue area is the part that is visible after applying the clip-path. I only use blue to illustrate the concept, but in reality, we only see shadows within the area. As you can see, we define four points, and their values ​​are very large (B). My big value is 100vmax, but it can be whatever big value you want. The idea is to make sure we have enough shadow space. We have four more points that are the corner points of the pseudo-element.

Arrows indicate the path that defines the polygon. We start from (-B, -B) and until we reach (0,0). In total, we need 10 points. Not 8 points, because the two points ((-B,-B) and (0,0)) are repeated twice in the path.

We also need to do the last thing , which is to consider the diffusion distance and the offset. The above demonstration works simply because this is a special case, both the offset and diffusion distance are equal to 0.

Let's define diffusion and see what happens. Remember, we use negative value inset to do this:

The pseudo-element is now larger than the main element, so the clip-path clip is more than we need. Remember, we always need to crop the part of the main element

inside (the area inside the green border in the example). We need to adjust the position of the four points in the clip-path.

<code>.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* 控制扩散 */
  transform: translate(10px, 8px); /* 控制偏移量 */
  z-index: -1; /* 将元素置于后面 */
  background: /* 你的渐变色在这里 */;
  filter: blur(10px); /* 控制模糊 */
}</code>
We defined a CSS variable --s for diffusion distance and updated the polygon points. I didn't touch where I used the big value. I only update the points that define the angle of the pseudo-element. I increase all zero values ​​--s and reduce 100% of the values ​​--s.

The offset is the same logic. When we convert pseudo-elements, the shadows are misaligned and we need to correct the polygon again and move the points to the opposite direction.

<code>box-shadow: 10px 8px 10px 5px orange;</code>
There are two more variables for offsets: --x and --y. We use them inside transform and we also updated the clip-path value. We still don't touch polygon points with large values, but we offset all other points - we reduce --x from X coordinates, and --y from Y coordinates.

Now we only need to update some variables to control the gradient shadow. When we do this, let's also turn the fuzzy radius into a variable:

Do we still need 3D conversion skills?

It all depends on the border. Don't forget that the reference for pseudo-elements is the fill box, so if you apply borders to the main element, you will have overlap. You can keep the 3D conversion trick or update the inset value to consider borders.

This is the version used to replace 3D conversion with the updated inset value in the previous demonstration:

I think this is a more appropriate approach, because the diffusion distance will be more accurate, as it starts with border-box instead of padding-box. But you need to adjust the inset value according to the border of the main element. Sometimes the border of the element is unknown and you have to use the previous solution.

Using the previous non-transparent solution, you may encounter stacking context issues. With transparent solutions, you may encounter border issues. Now you have options and ways to solve these problems. 3D conversion trick is my favorite solution as it fixes all issues (online generators will consider it too)

Add rounded corners

If you try to add border-radius to elements when using the non-transparent solution we started, this is a fairly simple task. You just need to inherit the same value from the main element and you're done.

Defining border-radius: inherit is a good idea even if you don't have rounded corners. This considers any potential rounded corners you may want to add in the future or rounded corners from elsewhere.

The situation of dealing with transparent solutions is different. Unfortunately, this means finding another solution because clip-path cannot handle the curve. This means we will not be able to crop the area inside the main element.

We add the mask attribute to the blend.

This part is very tedious and I have a hard time finding a general solution that does not rely on magic numbers. I ended up with a very complex solution that only uses one pseudo-element, but the code is a mess and covers only some specific situations. I don't think it's worth exploring this path.

To simplify the code, I decided to insert an extra element. The following are the marks:

<code>.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* 控制扩散 */
  transform: translate(10px, 8px); /* 控制偏移量 */
  z-index: -1; /* 将元素置于后面 */
  background: /* 你的渐变色在这里 */;
  filter: blur(10px); /* 控制模糊 */
}</code>

I use custom element to avoid any potential conflicts with external CSS. I can use

, but because it is a common element, it's easy to be positioned by another CSS rule from elsewhere, which can break our code. The first step is to locate the element and deliberately create the overflow:
<code>box-shadow: 10px 8px 10px 5px orange;</code>

The code may look a little strange, but we will explain the logic behind it step by step. Next, we use the pseudo-element of to create a gradient shadow.

<code>.box {
  position: relative;
  transform-style: preserve-3d;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px;
  transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
  background: /* .. */;
  filter: blur(10px);
}</code>

As you can see, the pseudo-element uses the same code as all previous examples. The only difference is that the 3D transformation is defined on the element, not on the pseudo-element. Currently, we have a gradient shadow without transparency function:

Please note that the area of ​​the element is defined with a black outline. Why do I do this? Because of this, I can apply a mask on it to hide the parts inside the green area and keep the overflowing part where we need to see the shadow.

I know this is a bit tricky, but unlike clip-path, the mask attribute does not take into account the area of ​​the element outside to show and hide the content. That's why I had to introduce extra elements – to simulate the "outer" area.

Also, note that I'm using a combination of borders and inset to define the area. This allows me to keep the padding-box of the extra element the same as the main element so that the pseudo-element does not require additional calculations.

Another useful thing that gets from using extra elements is that the elements are fixed and only the pseudo-elements are moving (using translate). This will allow me to easily define the mask, which is the last step of this trick.

Completed! We have gradient shadows, and it supports border-radius! You might expect a complex mask value with lots of gradients inside, but not! We only need two simple gradients and a mask-composite to complete the magic.
<code>.box {
  position: relative;
  z-index: 0; /* 我们强制创建一个堆叠上下文 */
}
/* 创建阴影 */
.box::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: -5px;
  transform: translate(10px, 8px);
  background: /* .. */;
  filter: blur(10px);
}
/* 复制主元素样式 */
.box::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  /* 继承在主元素上定义的所有装饰 */
  background: inherit;
  border: inherit;
  box-shadow: inherit;
}</code>

Let's isolate the

element to understand what's going on there:

The following are the results we got:
<code>clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0)</code>

Notice how the internal radius matches the border-radius of the main element. I defined a large border (150px) and a border-radius equal to the large border

plus

main element radius. Externally, I have a radius equal to 150px R. Internally, I have 150px R - 150px = R. We have to hide the inner (blue) part and make sure the border (red) part is still visible. To do this, I defined two mask layers - one covering only the content-box area and the other covering the border-box area (default). I then exclude one from the other to show the border.

<code>.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* 控制扩散 */
  transform: translate(10px, 8px); /* 控制偏移量 */
  z-index: -1; /* 将元素置于后面 */
  background: /* 你的渐变色在这里 */;
  filter: blur(10px); /* 控制模糊 */
}</code>

I use the same technique to create borders that support gradients and border-radius. Ana Tudor also has a good article about mask compounding, and I invite you to read it.

Are there any disadvantages of this method?

Yes, this is definitely not perfect. The first problem you may encounter is related to using borders on the main element. If you don't consider it, this can lead to a slight misalignment of the radius. This problem exists in our example, but you may have a hard time noticing it.

Fixed relatively easy: add border width to the inset of the element.

<code>box-shadow: 10px 8px 10px 5px orange;</code>

Another disadvantage is the large value we use for borders (150px in the example). This value should be large enough to contain shadows, but not too large to avoid overflow and scrollbar issues. Fortunately, the online generator will take all parameters into account to calculate the optimal value.

The last drawback I realize is when you use complex border-radius. For example, if you want to apply a different radius to each corner, you have to define a variable for each side. This is not a real drawback, I think, but it may make your code a little harder to maintain.

<code>.box {
  position: relative;
  transform-style: preserve-3d;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px;
  transform: translate3d(10px, 8px, -1px); /* (X, Y, Z) */
  background: /* .. */;
  filter: blur(10px);
}</code>

For simplicity, the online generator only considers a uniform radius, but you now know how to modify the code if you want to consider complex radius configurations.

Summary

We have reached the end! The magic behind the gradient shadow is no longer a mystery. I'm trying to cover all possibilities and any issues you might have. If I missed something or you found any issues, feel free to report it in the comments section and I will check it out.

Again, considering that the de facto solution will cover most of your use cases, many of these may be redundant. However, it is good to understand the “why” and “how” behind the technique and how to overcome its limitations. Also, we got a great practice in playing CSS editing and matte.

Of course, you can always use the online generator to avoid trouble.

The above is the detailed content of Different Ways to Get CSS Gradient Shadows. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Previous article:Moving BackgroundsNext article:Moving Backgrounds