Different Ways to Get CSS Gradient Shadows
Tags: article, boxshadow, gradients, shadow
Published: 2023-08-21 21:02:35

---
title: Different Ways to Get CSS Gradient Shadows
published: true
date: 2023-02-10 15:13:42 UTC
tags: Article,boxshadow,gradients,shadow
canonical_url: https://css-tricks.com/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!

Before we start, let me introduce [my gradient shadows generator](https://css-generators.com/gradient-shadows/). All you have to do is to adjust the configuration, and get the code. But follow along because I’m going to help you understand all the logic behind the generated code.

## Table of Contents

- [Non-transparent solution](#nontransparent-solution)
- [Transparent solution](#transparent-solution)
- [Adding a border radius](#adding-a-border-radius)
- [Wrapping up](#wrapping-up)

### Non-transparent solution

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.

The solution is to rely on a pseudo-element where the gradient is defined. You place it behind the actual element and [apply a blur filter to it](https://css-tricks.com/almanac/properties/f/filter/).

```
.box {
  position: relative;
}
.box::before {
  content: "";
  position: absolute;
  inset: -5px; /* control the spread */
  transform: translate(10px, 8px); /* control the offsets */
  z-index: -1; /* place the element behind */
  background: /* your gradient here */;
  filter: blur(10px); /* control the blur */
}
```

It looks like a lot of code, and that’s because it is. Here’s how we could have done it with a [`box-shadow`](https://css-tricks.com/almanac/properties/b/box-shadow/) instead if we were using a solid color instead of a gradient.

```
box-shadow: 10px 8px 10px 5px orange;
```

That should give you a good idea of what the values in the first snippet are doing. We have X and Y offsets, the blur radius, and the spread distance. Note that we need a negative value for the spread distance that comes from the [`inset`](https://css-tricks.com/almanac/properties/i/inset/) property.

Here’s a demo showing the gradient shadow next to a classic `box-shadow`:

<iframe id="cp_embed_xxJWzzz/27ea51cfa0ddc49a9ae80ea87e6b7042" src="//codepen.io/anon/embed/xxJWzzz/27ea51cfa0ddc49a9ae80ea87e6b7042?height=450&amp;theme-id=1&amp;slug-hash=xxJWzzz/27ea51cfa0ddc49a9ae80ea87e6b7042&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed xxJWzzz/27ea51cfa0ddc49a9ae80ea87e6b7042" title="CodePen Embed xxJWzzz/27ea51cfa0ddc49a9ae80ea87e6b7042" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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.

This solution is good, but still has a few drawbacks related to the `z-index: -1` declaration. Yes, there is [“stacking context”](https://css-tricks.com/its-always-the-stacking-context/) happening there!

<iframe id="cp_embed_eYjMKqM/2b08f902b70e8fd8d6c63a1c850b6c96" src="//codepen.io/anon/embed/eYjMKqM/2b08f902b70e8fd8d6c63a1c850b6c96?height=450&amp;theme-id=1&amp;slug-hash=eYjMKqM/2b08f902b70e8fd8d6c63a1c850b6c96&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed eYjMKqM/2b08f902b70e8fd8d6c63a1c850b6c96" title="CodePen Embed eYjMKqM/2b08f902b70e8fd8d6c63a1c850b6c96" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

I applied a [`transform`](https://css-tricks.com/almanac/properties/t/transform/) to the main element, and boom! The shadow is no longer below the element. This is not a bug but the logical result of a stacking context. Don’t worry, I will not start a boring explanation about stacking context ([I already did that in a Stack Overflow thread](https://stackoverflow.com/a/54903621/8620333)), but I’ll still show you how to work around it.

The first solution that I recommend is to use a 3D `transform`:

```
.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);
}
```

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.

<iframe id="cp_embed_xxJWJbm/fea263d6c68838cc0d1ca7dc2c09a905" src="//codepen.io/anon/embed/xxJWJbm/fea263d6c68838cc0d1ca7dc2c09a905?height=450&amp;theme-id=1&amp;slug-hash=xxJWJbm/fea263d6c68838cc0d1ca7dc2c09a905&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed xxJWJbm/fea263d6c68838cc0d1ca7dc2c09a905" title="CodePen Embed xxJWJbm/fea263d6c68838cc0d1ca7dc2c09a905" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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 — `::before` and `::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.

```
.box {
  position: relative;
  z-index: 0; /* We force a stacking context */
}
/* Creates the shadow */
.box::before {
  content: "";
  position: absolute;
  z-index: -2;
  inset: -5px;
  transform: translate(10px, 8px);
  background: /* .. */;
  filter: blur(10px);
}
/* Reproduces the main element styles */
.box::after {
  content: """;
  position: absolute;
  z-index: -1;
  inset: 0;
  /* Inherit all the decorations defined on the main element */
  background: inherit;
  border: inherit;
  box-shadow: inherit;
}
```

<iframe id="cp_embed_rNrdrWZ/58c29bf226f701daf9430aba16e9fcaa" src="//codepen.io/anon/embed/rNrdrWZ/58c29bf226f701daf9430aba16e9fcaa?height=450&amp;theme-id=1&amp;slug-hash=rNrdrWZ/58c29bf226f701daf9430aba16e9fcaa&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed rNrdrWZ/58c29bf226f701daf9430aba16e9fcaa" title="CodePen Embed rNrdrWZ/58c29bf226f701daf9430aba16e9fcaa" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

It’s important to note that we are _forcing_ the main element to create a stacking context by declaring `z-index: 0`, or [any other property that do the same](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context#description), on it. Also, don’t forget that pseudo-elements consider the padding box of the main element as a reference. So, if the main element has a border, you need to take that into account when defining the pseudo-element styles. You will notice that I am using `inset: -2px` on `::after` to account for the border defined on the main element.

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.

### Transparent solution

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 `0`.

<iframe id="cp_embed_dyjmjaM/e16bf3655086e062830a818b9133484c" src="//codepen.io/anon/embed/dyjmjaM/e16bf3655086e062830a818b9133484c?height=450&amp;theme-id=1&amp;slug-hash=dyjmjaM/e16bf3655086e062830a818b9133484c&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed dyjmjaM/e16bf3655086e062830a818b9133484c" title="CodePen Embed dyjmjaM/e16bf3655086e062830a818b9133484c" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

The idea is to find a way to cut or hide everything inside the area of the element (inside the green border) while keeping what is outside. We are going to use [`clip-path`](https://css-tricks.com/almanac/properties/c/clip-path/) for that. But you might wonder how `clip-path` can make a cut _inside_ an element.

Indeed, there’s no way to do that, but we can simulate it using a particular polygon pattern:

```
clip-path: polygon(-100vmax -100vmax,100vmax -100vmax,100vmax 100vmax,-100vmax 100vmax,-100vmax -100vmax,0 0,0 100%,100% 100%,100% 0,0 0)
```

<iframe id="cp_embed_RwBMBmp/79f53e14661aa0ac1684079eb8162ff0" src="//codepen.io/anon/embed/RwBMBmp/79f53e14661aa0ac1684079eb8162ff0?height=450&amp;theme-id=1&amp;slug-hash=RwBMBmp/79f53e14661aa0ac1684079eb8162ff0&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed RwBMBmp/79f53e14661aa0ac1684079eb8162ff0" title="CodePen Embed RwBMBmp/79f53e14661aa0ac1684079eb8162ff0" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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.

![Showing the clip-path coordinates for the element.](https://i0.wp.com/css-tricks.com/wp-content/uploads/2023/01/s_48C6EEC0315F934C02900CCBCCCDFBD0A1B256CE69F7A4B8A991D9F184217B87_1674637694432_image.png?resize=763%2C502&ssl=1)

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 (`(-B,-B)` and `(0,0)`).

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 `0`.

Let’s define the spread and see what happens. Remember that we use `inset` with a negative value to do this:

<iframe id="cp_embed_YzjaMOY/b3df7064381094b61c26687918bebf19" src="//codepen.io/anon/embed/YzjaMOY/b3df7064381094b61c26687918bebf19?height=450&amp;theme-id=1&amp;slug-hash=YzjaMOY/b3df7064381094b61c26687918bebf19&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed YzjaMOY/b3df7064381094b61c26687918bebf19" title="CodePen Embed YzjaMOY/b3df7064381094b61c26687918bebf19" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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 `clip-path`.

```
.box {
  --s: 10px; /* the spread */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px + var(--s)) calc(0px + var(--s)),
    calc(0px + var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(100% - var(--s)),
    calc(100% - var(--s)) calc(0px + var(--s)),
    calc(0px + var(--s)) calc(0px + var(--s))
  );
}
```

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 `--s`.

<iframe id="cp_embed_jOpzRdg/26b06416f709fa6f69cd9a328f6e7a79" src="//codepen.io/anon/embed/jOpzRdg/26b06416f709fa6f69cd9a328f6e7a79?height=450&amp;theme-id=1&amp;slug-hash=jOpzRdg/26b06416f709fa6f69cd9a328f6e7a79&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed jOpzRdg/26b06416f709fa6f69cd9a328f6e7a79" title="CodePen Embed jOpzRdg/26b06416f709fa6f69cd9a328f6e7a79" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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.

```
.box {
  --s: 10px; /* the spread */
  --x: 10px; /* X offset */
  --y: 8px; /* Y offset */
  position: relative;
}
.box::before {
  inset: calc(-1 * var(--s));
  transform: translate3d(var(--x), var(--y), -1px);
  clip-path: polygon(
    -100vmax -100vmax,
     100vmax -100vmax,
     100vmax 100vmax,
    -100vmax 100vmax,
    -100vmax -100vmax,
    calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y)),
    calc(0px + var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(100% - var(--s) - var(--y)),
    calc(100% - var(--s) - var(--x)) calc(0px + var(--s) - var(--y)),
    calc(0px + var(--s) - var(--x)) calc(0px + var(--s) - var(--y))
  );
}
```

There are two more variables for the offsets: `--x` and `--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:

<iframe id="cp_embed_poZLmgj/7a0244a4dca0b363791881fe50f235f8" src="//codepen.io/anon/embed/poZLmgj/7a0244a4dca0b363791881fe50f235f8?height=550&amp;theme-id=1&amp;slug-hash=poZLmgj/7a0244a4dca0b363791881fe50f235f8&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed poZLmgj/7a0244a4dca0b363791881fe50f235f8" title="CodePen Embed poZLmgj/7a0244a4dca0b363791881fe50f235f8" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

> Do we still need the 3D `transform` trick?

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 `transform`:

<iframe id="cp_embed_NWBYVRK/bf9c95af498115ea9f2819d5aa6505b8" src="//codepen.io/anon/embed/NWBYVRK/bf9c95af498115ea9f2819d5aa6505b8?height=550&amp;theme-id=1&amp;slug-hash=NWBYVRK/bf9c95af498115ea9f2819d5aa6505b8&amp;default-tab=result" height="550" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed NWBYVRK/bf9c95af498115ea9f2819d5aa6505b8" title="CodePen Embed NWBYVRK/bf9c95af498115ea9f2819d5aa6505b8" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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.

With the earlier non-transparent solution, it’s possible you will face a stacking context issue. And with the transparent solution, it’s possible you face a border issue instead. Now you have options and ways to work around those issues. The 3D transform trick is my favorite solution because it fixes all the issues ([The online generator](https://css-generators.com/gradient-shadows/) will consider it as well)

### Adding a border radius

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.

<iframe id="cp_embed_abjYrYq/652ab5c0da5084a21bb11345ed426273" src="//codepen.io/anon/embed/abjYrYq/652ab5c0da5084a21bb11345ed426273?height=450&amp;theme-id=1&amp;slug-hash=abjYrYq/652ab5c0da5084a21bb11345ed426273&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed abjYrYq/652ab5c0da5084a21bb11345ed426273" title="CodePen Embed abjYrYq/652ab5c0da5084a21bb11345ed426273" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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.

We will introduce the [`mask`](https://css-tricks.com/almanac/properties/m/mask/) property to the mix.

This part was very tedious, and I struggled to find a general solution that doesn’t rely on [magic numbers](https://css-tricks.com/magic-numbers-in-css/). I ended up with a very complex solution that uses only one pseudo-element, but the code was a lump of spaghetti that covers only a few particular cases. I don’t think it is worth exploring that route.

I decided to insert an extra element for the sake of simpler code. Here’s the markup:

```
<div class="box">
  <sh></sh>
</div>
```

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:

```
.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}
```

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 `<sh>`.

```
.box {
  --r: 50px;
  position: relative;
  border-radius: var(--r);
  transform-style: preserve-3d;
}
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
  transform: translateZ(-1px)
}
.box sh::before {
  content: "";
  position: absolute;
  inset: -5px;
  border-radius: var(--r);
  background: /* Your gradient */;
  filter: blur(10px);
  transform: translate(10px,8px);
}
```

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:

<iframe id="cp_embed_MWBGJoV/ccdb3fd5c762a95f1d580f02babfba3f" src="//codepen.io/anon/embed/MWBGJoV/ccdb3fd5c762a95f1d580f02babfba3f?height=600&amp;theme-id=1&amp;slug-hash=MWBGJoV/ccdb3fd5c762a95f1d580f02babfba3f&amp;default-tab=result" height="600" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed MWBGJoV/ccdb3fd5c762a95f1d580f02babfba3f" title="CodePen Embed MWBGJoV/ccdb3fd5c762a95f1d580f02babfba3f" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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 `clip-path`, the `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 `border` and `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.

Another useful thing we get from using an extra element is that the element is fixed, and only the pseudo-element is moving (using `translate`). This will allow me to easily define the mask, which is the _last_ step of this trick.

```
mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;
```

<iframe id="cp_embed_ExpLZMz/2eeab3ea368b67035e79fea37fa52e83" src="//codepen.io/anon/embed/ExpLZMz/2eeab3ea368b67035e79fea37fa52e83?height=450&amp;theme-id=1&amp;slug-hash=ExpLZMz/2eeab3ea368b67035e79fea37fa52e83&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed ExpLZMz/2eeab3ea368b67035e79fea37fa52e83" title="CodePen Embed ExpLZMz/2eeab3ea368b67035e79fea37fa52e83" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

It’s done! We have our gradient shadow, and it supports `border-radius`! You probably expected a complex `mask` value with oodles of gradients, but no! We only need two simple gradients and a `mask-composite` to complete the magic.

Let’s isolate the `<sh>` element to understand what is happening there:

```
.box sh {
  position: absolute;
  inset: -150px;
  border: 150px solid red;
  background: lightblue;
  border-radius: calc(150px + var(--r));
}
```

Here’s what we get:

<iframe id="cp_embed_yLqjMLQ/6d7ee140a1d1c2906543547dfc7489dd" src="//codepen.io/anon/embed/yLqjMLQ/6d7ee140a1d1c2906543547dfc7489dd?height=600&amp;theme-id=1&amp;slug-hash=yLqjMLQ/6d7ee140a1d1c2906543547dfc7489dd&amp;default-tab=result" height="600" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed yLqjMLQ/6d7ee140a1d1c2906543547dfc7489dd" title="CodePen Embed yLqjMLQ/6d7ee140a1d1c2906543547dfc7489dd" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

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.

```
mask:
  linear-gradient(#000 0 0) content-box,
  linear-gradient(#000 0 0);
mask-composite: exclude;
```

<iframe id="cp_embed_QWBrpjB/e8d4fb927227a5e3682e46220d9c9af0" src="//codepen.io/anon/embed/QWBrpjB/e8d4fb927227a5e3682e46220d9c9af0?height=600&amp;theme-id=1&amp;slug-hash=QWBrpjB/e8d4fb927227a5e3682e46220d9c9af0&amp;default-tab=result" height="600" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed QWBrpjB/e8d4fb927227a5e3682e46220d9c9af0" title="CodePen Embed QWBrpjB/e8d4fb927227a5e3682e46220d9c9af0" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

I used the same technique to [create a border that supports gradients and `border-radius`](https://dev.to/afif/border-with-gradient-and-radius-387f). Ana Tudor has also a good article [about masking composite](https://css-tricks.com/mask-compositing-the-crash-course/) that I invite you to read.

> 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 `<sh>` element’s `inset`.

```
.box {
  --r: 50px;
  border-radius: var(--r);
  border: 2px solid;
}
.box sh {
  position: absolute;
  inset: -152px; /* 150px + 2px */
  border: 150px solid #0000;
  border-radius: calc(150px + var(--r));
}
```

Another drawback is the big value we’re using for the border (`150px` in the example). This value should be big enough to contain the shadow but not too big to avoid overflow and scrollbar issues. Luckily, [the online generator](https://css-generators.com/gradient-shadows/) will calculate the optimal value considering all the parameters.

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.

```
.box {
  --r-top: 10px;
  --r-right: 40px;
  --r-bottom: 30px;
  --r-left: 20px;
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
.box sh {
  border-radius: calc(150px + var(--r-top)) calc(150px + var(--r-right)) calc(150px + var(--r-bottom)) calc(150px + var(--r-left));
}
.box sh:before {
  border-radius: var(--r-top) var(--r-right) var(--r-bottom) var(--r-left);
}
```

<iframe id="cp_embed_LYBmWLR/9f04e99ed847d585938ab6202a231fc3" src="//codepen.io/anon/embed/LYBmWLR/9f04e99ed847d585938ab6202a231fc3?height=450&amp;theme-id=1&amp;slug-hash=LYBmWLR/9f04e99ed847d585938ab6202a231fc3&amp;default-tab=result" height="450" scrolling="no" frameborder="0" allowfullscreen allowpaymentrequest name="CodePen Embed LYBmWLR/9f04e99ed847d585938ab6202a231fc3" title="CodePen Embed LYBmWLR/9f04e99ed847d585938ab6202a231fc3" style="width:100%;overflow:hidden">CodePen Embed Fallback</iframe>

[The online generator](https://css-generators.com/gradient-shadows/) only considers a uniform radius for the sake of simplicity, but you now know how to modify the code if you want to consider a complex radius configuration.

### Wrapping up

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.

And, of course, you have [the online generator](https://css-generators.com/gradient-shadows/) you can reach for anytime you want to avoid the hassle.

* * *

<small><a rel="nofollow" href="https://css-tricks.com/different-ways-to-get-css-gradient-shadows/">Different Ways to Get CSS Gradient Shadows</a> originally published on <a rel="nofollow" href="https://css-tricks.com">CSS-Tricks</a>, which is part of the <a href="https://try.digitalocean.com/css-tricks/?utm_medium=rss&amp;utm_source=css-tricks.com&amp;utm_campaign=family_&amp;utm_content=">DigitalOcean</a> family. You should <a href="https://css-tricks.com/newsletters/">get the newsletter</a>.</small>