Skip to main content

Various Methods for Obtaining CSS Gradient Shadows

Various Methods for Obtaining CSS Gradient Shadows

The question of whether it is possible to create gradient shadows instead of solid color shadows? in CSS is often asked. Despite there being no specific CSS property for this, various CSS tricks can be used to approximate gradient shadows. However, these tricks typically only work when element has a non-transparent background.

This article focuses on exploring a solution for creating gradient shadows for elements with transparent backgrounds, which is not commonly seen. A gradient shadow generator will be introduced, where you can simply adjust configuration and obtain code. The author will also help to explain logic behind generated code.

Non-Transparent Backgrounds

To begin with, there's a solution that will be effective in 80% of situations. The most common scenario is when you are utilizing an element with a background and want to add a gradient shadow to it, with no transparency concerns.

The answer is to utilize a pseudo-element where the gradient is specified. Place it behind the actual element and apply a blur filter to it.

CSS

.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 */
}

The reason why code appears lengthy is that it is indeed extensive. However, if we had utilized a box-shadow and a solid color instead of a gradient, the code would have been more concise.

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

The explanation provided should provide you with a good understanding of how the values in the first code snippet work. These values include the X and Y offsets, the blur radius, and the spread distance. It is important to note that a negative value is needed for the spread distance when using the "inset" property.
Additionally, there is a demo available that displays a comparison between the gradient shadow effect and a classic box-shadow effect.

See the Pen Gradient shadow vs box-shadow by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

If you observe closely, you'll notice some dissimilarities between two shadows, particularly in the blurred section. This disparity is unsurprising as I believe the algorithm for the filter property works differently than the one for box-shadow. Nonetheless, the differences are insignificant, and the outcome is quite comparable. However, there are some limitations associated with the z-index: -1 declaration, as it creates a "stacking context."

See the Pen Various Methods for Obtaining CSS Gradient Shadows by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

After applying a transform to the primary element, I noticed that the shadow was no longer situated below it. This outcome is not an error, but rather a consequence of the stacking context. However, I won't delve into the details of this concept to avoid monotony (as I've already done so in a previous Stack Overflow thread). Instead, I'll provide you with a workaround to address the issue. My initial recommendation for a solution is to utilize a 3D transform:

CSS

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

To achieve the same effect as using "z-index: -1", we'll utilize a negative translation along the Z-axis and wrap everything inside "translate3d()". It's important to apply "transform-style: preserve-3d" to the main element, otherwise, the 3D transformation won't be applied.

See the Pen Fixing the z-index issue (3D solution) by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

To my knowledge, this solution doesn't seem to have any negative consequences. However, if you notice any side effects, please share them in the comment section, and we'll work together to find a solution. In case you're unable to employ a 3D transform, another alternative is to use two pseudo-elements (::before and ::after). One of the pseudo-elements generates the gradient shadow while the other mimics the primary background (along with any other styles required). By doing this, we can easily control the stacking order of both pseudo-elements.

CSS

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

				

See the Pen CodePen Home Fixing the z-index issue (pseudo element solution) by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

Firstly, it's worth highlighting that we are compelling the primary element to establish a stacking context by assigning z-index: 0, or any equivalent property to it. It's imperative to bear in mind that while dealing with pseudo-elements, the padding box of the primary element acts as a reference point. Therefore, in case the primary element has a border, you need to consider it while defining the styles for the pseudo-element. To tackle this, I am using inset: -2px on ::after to accommodate the border specified on the primary element. Although this approach is suitable for most cases where a gradient shadow is required, provided you don't require transparency. However, we are here to challenge ourselves and push the boundaries. So, even if you don't need the following solution, stay with me to explore new CSS techniques that you can apply elsewhere.

Technique for Transparent Backgrounds

To continue our work on the 3D transform and eliminate the background from the primary element, we'll resume from where we previously stopped. I'll initiate the process by creating a shadow that has zero offsets and spread distance.

See the Pen CodePen Home Fixing the z-index issue (pseudo element solution) by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

The concept is to discover a method to conceal or remove all content within the boundaries of an element (as defined by the green border) while retaining content outside of it. To accomplish this, we will utilize clip-path. You may be questioning how clip-path can create an incision inside an element. In reality, it's impossible to do so directly, but we can simulate it by utilizing a specific polygon pattern:

CSS

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

				

See the Pen CodePen Home Gradient shadow #1 by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

Behold! We now possess a gradient shadow that is capable of accommodating transparency. This was achieved simply by introducing a clip-path to the preceding code. A diagram has been included to demonstrate the polygonal aspect.

After applying the clip-path, the visible part is represented by the blue area. The blue color is solely used to explain the concept, but in reality, only the shadow inside the blue area will be seen. The corners of the pseudo-element are defined by four points, and four other points with a large value (B) are defined to provide enough space for the shadow. Although my big value is 100vmax, it can be any other value as long as it is sufficiently large. The path that defines the polygon is shown by the arrows, which start from (-B, -B) and end at (0,0). In total, we require 10 points, not 8, since two points are repeated twice in the path ((-B,-B) and (0,0)). To account for the spread distance and offsets, we need to perform one more task. The above demo only works because the offsets and spread distance are both equal to 0. Let's define the spread and observe its effect. Keep in mind that we use a negative value with the "inset" property to accomplish this.

See the Pen CodePen Home Adding spread distance by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

Due to the pseudo-element's increased size relative to the main element, the clip-path is cutting more than necessary. It's important to keep in mind that only the area inside the main element (i.e., the green border in the example) should be cut. To achieve this, the position of the four points within the clip-path needs to be adjusted.

CSS

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

				

A CSS variable called "--s" has been created to represent the spread distance, and the polygon points have been adjusted accordingly. The larger values have not been modified, but only the points that determine the corners of the pseudo-element have been updated. Specifically, all zero values have been increased by "--s", while all 100% values have been decreased by "--s".

See the Pen CodePen Home Gradient Shadow #2 by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

The process is analogous for the offsets. If we shift the pseudo-element using translation, the shadow becomes misaligned, and we must correct the polygon once more by moving the points in the opposite direction.

CSS

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

				

Two additional variables, "--x" and "--y", have been defined for the offsets. These variables are used within the "transform" function and also affect the "clip-path" values. Similar to before, the polygon points with large values remain untouched, but all others are offset by subtracting "--x" from their X coordinates and "--y" from their Y coordinates. At this point, we only need to modify a few more variables to manage the gradient shadow. As we're making these adjustments, let's also include the blur radius as a variable.

See the Pen CodePen Home Final gradient shadow with transparency by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

Is the 3D transform trick still necessary? Well, it really hinges on the border. It's important to keep in mind that the reference point for a pseudo-element is the padding box, which means that if a border is applied to the main element, there may be some overlap. In such cases, you can either continue using the 3D transform trick or adjust the inset value to accommodate for the border. To see this in action, check out the demo below, where the inset value has been modified instead of using the 3D transform trick.

See the Pen CodePen Home Updating inset instead of 3D transform by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

In my opinion, using the border-box as a reference point for the spread distance is a more appropriate approach, as it yields more accurate results compared to using the padding-box. However, you must adjust the inset value based on the border of the main element, which can be unknown in certain cases, requiring you to use the previous solution. If you choose to use the non-transparent solution from earlier, you may encounter a stacking context problem. Alternatively, the transparent solution may lead to border issues. Fortunately, you have several options available to overcome these challenges. Personally, I prefer the 3D transform technique since it resolves all these problems (the online generator also supports this approach).

Incorporating Border Radii

Incorporating border-radius into the non-transparent solution we initially used is a simple task. You can achieve this by inheriting the same value from the main element, and the process is complete.

See the Pen Add border-radius to non-transparent solution by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

It's a good practice to define "border-radius: inherit" even if you don't currently have a border-radius. This accounts for any future border-radius you may want to add or one that originates from another source. However, when working with the transparent solution, finding an alternative approach is necessary because clip-path cannot accommodate curved shapes. Therefore, we can't utilize it to cut out the area inside the main element. Instead, we can make use of the "mask" property. This process was quite laborious, and it was challenging to create a general solution that doesn't rely on arbitrary values. Although I eventually developed a complex solution that utilized only one pseudo-element, the code was convoluted and only applicable to specific scenarios. As such, I don't believe it's worth pursuing that path. To simplify the code, I chose to add an additional element. Here is the corresponding markup:

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

				

To prevent any possible clashes with external CSS, I opted to use a custom element, <sh>, rather than a <div>. Although a <div> is a common element, it's more susceptible to being affected by other CSS rules originating from elsewhere that can potentially disrupt our code. The initial step involves positioning the <sh> element and intentionally creating an overflow:

CSS

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

				

Although the code might appear unfamiliar, we will explore the reasoning behind it gradually. Afterward, we will generate the gradient shadow by utilizing a pseudo-element of <sh>.

CSS

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

				

It is evident that the pseudo-element employs identical code to the preceding examples, with the sole contrast being the 3D transform specified on the <sh> element instead of the pseudo-element. Currently, we possess a gradient shadow lacking the transparency aspect.

See the Pen Radius + transparency #1 by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

It's important to observe that the <sh> element's area is specified by the black outline for a specific purpose. By doing this, I can apply a mask to conceal the area within the green region while retaining the overflowing portion where the shadow should appear. Although clip-path is an option, the mask property doesn't consider the external area of an element for displaying or hiding content. Hence, I had to introduce an additional element to simulate the outside area. Furthermore, it's worth noting that I'm using a combination of border and inset to establish the area. This allows me to maintain the padding-box of the extra element identical to the main element, avoiding the necessity for further computations with the pseudo-element. The added element is stationary, and solely the pseudo-element is mobile (using translate). As a result, it's simple to define the mask, which is the final stage of this strategy.

CSS

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

				

See the Pen CodePen Home Final border radius with transparency by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

We did it! Our gradient shadow is now complete and supports border-radius. You may have anticipated a convoluted mask value with multiple gradients, but we only require two basic gradients and a mask-composite to accomplish this feat. To comprehend what's happening within the <sh> element, let's examine it in isolation.

CSS

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

				

This is outcome:

See the Pen Overview of sh element by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

Observe how the internal radius corresponds to the border-radius of the primary element. I've set a broad border (150px) and a border-radius equivalent to the big border plus the primary element's radius. The external area has a radius equal to 150px + R, and the internal region has 150px + R - 150px = R. To conceal the inner (blue) part while ensuring the visibility of the border (red) part, I've defined two mask layers - one covering solely the content-box area and the other covering the border-box area (default value). Then, by excluding one from the other, the border is exposed.

CSS

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

				

See the Pen CodePen Home Adding mask to sh element by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

I applied the identical approach for producing a border that accommodates both gradients and border-radius. Additionally, I recommend that you read Ana Tudor's informative article on masking composites. Is there any downside to using this technique? Undoubtedly, this method is not flawless. One issue that may arise is related to applying a border to the primary element, which can result in a slight misalignment in the radii if not taken into account. In our example, this issue exists, but it may be barely noticeable. The remedy for this problem is relatively straightforward: Include the border's width in the <sh> element's inset.

CSS

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

				

One more limitation to this technique is the large border value used in the example (150px), which must be sufficient to accommodate the shadow while avoiding any overflow and scrollbar problems. Fortunately, the online generator will compute the ideal value, considering all of the parameters. Another shortcoming is evident when dealing with a complex border-radius. Suppose you want to apply a distinct radius to each corner; in that case, you must define a variable for each side. Although this may not be considered a significant drawback, it can make your code more difficult to maintain.

CSS

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

				

See the Pen CodePen Home Complex border radius configuration by Mushtaq Ahmad ( @MushtaqPhysicist) on CodePen.

To simplify the process, the online generator only accounts for a uniform radius. However, now that you have learned how to adjust the code, you can incorporate a more intricate radius configuration if needed.

Concluding Thoughts

We have reached the end of this discussion, and the mystery behind gradient shadows has been unravelled. I have endeavored to cover all potential scenarios and issues that may arise, but if I have missed anything or if you encounter any problems, please do not hesitate to report them in the comments section, and I will look into it. While the de facto solution will likely suffice for most use cases, it is still beneficial to understand the "why" and "how" behind the technique and how to overcome its limitations. Additionally, we had a chance to practice using CSS clipping and masking, which was a good exercise. Lastly, remember that you can always access the online generator if you wish to avoid any inconvenience.

Comments

Post a Comment

Hello,

Thank you for taking the time to leave a comment! Your feedback is important to us and we appreciate any suggestions or thoughts you may have. We strive to create the best possible experience for our users and your comments will help us achieve that goal.

If you have any questions or concerns, please don't hesitate to reach out to us. We're here to help and are always happy to hear from our users.

Thank you again for your comment and have a great day!

Best regards,
NewWebSofts

Popular posts from this blog

How to Create a Circular Scroll Progress with HTML, CSS & JavaScript

See the Pen How to Create a Circular Scroll Progress with HTML, CSS & JavaScript by Mushtaq Ahmad ( @MushtaqPhysicist ) on CodePen . How to Create a Circular Scroll Progress with HTML, CSS & JavaScript In this guide, we will explore process of using HTML, CSS, and JavaScript to design a circular scroll progress indicator. This type of indicator displays percentage of a webpage that has been scrolled. Specifically, we will create a circular indicator and position it in bottom right corner of webpage. The indicator's fill level will correspond to amount of page that has been scrolled. HTML Copy Code <!DOCTYPE html> <html lang="en"> <head> <title>Scroll Percentage</title> <!-- Stylesheet --> <link rel="stylesheet" href="style.css"> </head> <body>...

Find Your Location using HTML, CSS & JavaScript

See the Pen Untitled by Mushtaq Ahmad ( @MushtaqPhysicist ) on CodePen . Find Your Location using HTML, CSS & JavaScript It is against policy to access Geolocation in Blogger.com . However, you can still find your location by clicking on CodePen button and then clicking the Get Location button to retrieve your location data Source Code HTML Copy Code <div class="container"> <h1>Find Your Location</h1> <p>Your location: <span id="location"></span></p> <button id="getLocation">Get Location</button> </div> CSS Copy Code body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f5f5f5; } .container { max-width: 800px; margin: 0 auto; padding: 50px; background-color: #fff; border...

Develop a Quiz App with Timer using HTML CSS & JavaScript

Develop a Quiz App with Timer using HTML CSS & JavaScript See the Pen Create a Quiz App with Timer using HTML CSS & JavaScript by Mushtaq Ahmad ( @MushtaqPhysicist ) on CodePen . In this article you’ll learn how to Create a Quiz Application with Timer using HTML CSS & JavaScript. The Quiz App with Timer program consists of three layers or boxes that are sequentially displayed when a specific button is clicked. Initially, the webpage displays a "Start Quiz" button, which, upon clicking, triggers a pop-up animation displaying an info box. The info box contains the rules of the quiz and two buttons labeled "Exit" and "Continue." Clicking on the "Exit" button hides the info box, while clicking on "Continue" button displays the Quiz Box. Within the Quiz Box, there is a header with a title on the left side and a timer box on the right side. The timer starts at 15 seconds and decremen...