Shadows in user interfaces are usually transparent, black blurs. That’s fine if the UI only has white or gray backgrounds, but such shadows can look like grimy blots if they’re cast on a brightly colored surface.
Gray shadows on gray surfaces look natural. On other colors, such gray shadows look dirty.
In UI design, we usually consider shadow a property of the element that casts it. But in reality, shadows are (sidenote: Josh Comeau has a nice, visual way of explaining how shadows work and how to render them well. ) several factors:
- The light sources and their positioning, shape, and brightness. These define the contrast between shadows and their surroundings—the darkness of the shadow.
- The object that blocks the light source, creating the shape of the shadow.
- The distance between the object and the surface that, together with the shape of the light source, defines the shadow’s contour hardness—how blurry it looks.
- The surface on which the shadow is cast, defining the color of the shadow.
We render shadows as if there’s only ambient lighting and a single far-away diffused light source. That light source is usually at the top left or at the top center. It’s a constant across the whole UI, so it can very well be defined by fixed values applied to global shadow styles. As the shape of the shadow should actually be defined by the object that is casting it, it feels useful to define shadows as the object’s property, so for that factor, it totally makes sense to define shadows as a property of such an object. But the color of the shadow should be based on the color of the surface on which it’s rendered!
Just look at this:
Square number 1 has the color of the wall that’s lit directly by the sun. Square number 2 has the color of that same wall’s part that’s in the shadow. The color of square 3 I created myself: it’s square 1’s color mixed with black until it had the same (subjective) brightness as square 2. The result is a dull beige compared to square 2’s teak.
Unless a design has to look neutral, I avoid using neutral greys for UI backgrounds. They’re boring. Mixing in a little color with the background can really set a design’s mood. Now we wouldn’t want shadows causing such a design to look grimy. We need shadows with the same tint as the surface they’re cast upon:
There are two issues with that approach though:
- Since time immemorial, design and illustration software and UI libraries define shadows as a property of the object casting them.
- UI components with shadows may be dynamically placed on a whole range of surface colors. We’d need shadow styles for each.
Unless there’s less than a handful of background colors in the design, specifying a shadow color for each element based on the color of the surface it sits on is prone to errors. In some cases, it’s even unfeasible, like on websites where elements are dynamically placed on backgrounds.
Therefore, the surface on which an object is placed should define the shadow color. This could be tricky because colored surfaces may be placed on other colored surfaces. That means the object casting a shadow itself may have a background color too, which in turn has to define the color of shadows that are cast on that.
The perfect case for CSS custom properties? Let’s find out!
Saturated, parametric shadow colors through CSS custom properties
If you’re a designer who doesn’t write CSS, this section may look a bit off-putting, but if you want to convince your web development coworker to implement the idea above, you may want to take a quick look at it anyway. It requires a bit of an unusual approach and without it, it may seem unfeasible.
I figured that by using custom properties and OKLCH colors, I could define the chroma (saturation) and hue values of each color separately. With that, I can create lightness variations for each type of color and create classes for background surfaces. Then, for each surface color, I could create a darker shadow color for the elements that are placed upon that surface. Something like:
/* Spoiler: this does not work! */
.has-background {
background-color: oklch(0.9 var(--hue-background-ch));
.has-box-shadow{
box-shadow: 0 1em 1em oklch(0.6 var(--hue-background-ch) / 0.4);
}
}
.has-background--red{
--hue-background-ch: 0.2 0;
}
.has-background--green{
--hue-background-ch: 0.2 150;
}
Similar classes could be added for more types of colors (yellow, blue, etc.). We can even expand the system with different lightness values for darker and lighter surface color variations. And for the surfaces, that works fine. For the shadows: not so much though, as you can see in this Codepen.
One problem with this code is that classes that set the hue-background-ch property also affect the value for the elements that have the has-box-shadow class. As a result, red objects get red shadows and green objects green shadows—regardless of the surface they’re placed upon. That’s precisely not what I wanted! I decided to include it here anyway, because it may help convey the basic idea and because one could think it’s a better solution than the one I’m going to show now.
It looks like we need to scope the assignment of shadows only to nested elements that aren’t supposed to get another shadow color. So I begin by assigning the background colors only to direct children of the element that has a background color. That, of course, makes the approach too rigid; maybe the direct child is a wrapper that shouldn’t cast a shadow, but contains elements that have shadow. So I also include other descendants that don’t have a background color. Like this:
.has-background {
background-color: oklch(0.9 var(--hue-background-ch));
>.has-box-shadow,
:not(.has-background)>.has-box-shadow {
box-shadow: 0 1em 1em oklch(0.6 var(--hue-background-ch) / 0.4);
}
}
.has-background--red{
--hue-background-ch: 0.2 0;
}
.has-background--green{
--hue-background-ch: 0.2 150;
}
With the additional color types and brightness variations, such a system could look like this:
See the Pen Tinted shadows matching backgrounds through inheriting custom properties (OKLCH) (@kslstn) on CodePen.
Some more things to consider with this approach:
- CSS authors must assign shadows by applying a class to an element. This is not consistent with defining the full shadow as a custom property, a popular approach in many design systems today. That said, the shadow color and brightness properties can, of course, be set in other classes too, so it’s not impossible to apply consistent shadow colors manually to an element. Arguably, the shadows themselves aren’t design tokens (the colors and sizes used to define the shadow properties are!).
- Each background hue (sidenote: I spent at least an hour trying to get rid of the repetition. I like parametrically defining colors because typically it lets me avoid that kind of repetition. Can’t see how I can get around it though. Looking forward to someone commenting how it can be done! ) of six near-identical lines just to create a new background hue class. Then again: you only do this once when you add a new hue and how many are you going to need? Even big design systems often only have around 10 hues.
- My example uses
oklch()which, like other CSS color functions, has okay but not great browser support. The general setup of letting a parent define the shadow color for descendants doesn’t require the parametric color mixing approach though—it only makes it a lot easier to add shades. It does require the:not()selector, but that has solid support. - We can expand the approach to support sibling elements, so a full-width section can cast a tinted shadow on the next. This requires using the
:has()function to select a previous sibling, which was introduced a little earlier than the color functions.
Too good to be true?
The approach above assumes that an element casts its shadow on an ancestor element with an even color. Applications for that could be a card casting a shadow on the section in which it appears or a button casting a shadow on its menu backdrop. In practice, elements may cast a shadow on other sorts of elements too.
A sticky header navigation, a modal window or a call to action on top of an image all may cast shadows on elements of changing or unknown colors.
This is a problem because there’s only one thing worse than a gray smudgy shadow and that’s a tinted shadow that doesn’t match the background!
What we really need is something like a darkening layer that can darken elements below it without affecting their saturation or hue. In CSS, the mix-blend-mode can’t be applied to the shadow property; it’s something that can only be applied to elements. So I considered rendering my shadows from scratch using blurred pseudo elements behind the element that needs a shadow. But this too is impossible without having to wrap each such element into a wrapper element to create a stacking context that allows it to put the shadow behind the element. This isn’t just convoluted, but also severely restricts positioning and layering elements using z-index.
It’s not all bad though! I’ve been applying tinted shadows in my designs using Figma’s Effect styles and implementing these in CSS using utility classes for years now. As it is error-prone, I believe the approach in the demo is superior. It’s easily expanded with utility shadow classes like has-shadow--gray. With that, we can assign gray shadows to elements that lie on top of images and elements that are not nested inside the element they cast their shadow on.
And I’ve got to admit: I don’t find the CSS implementation very easy. It took me hours to create the demo, time spent mostly with unsuccessful attempts to make the code more concise. I’m curious if you can come up with a better solution!