In my humble opinion, the best websites and web applications have a tangible “real” quality to them. There are lots of factors involved to achieve this quality, but shadows are a critical ingredient.
When I look around the web, though, it’s clear that most shadows aren’t as rich as they could be. The web is covered in fuzzy grey boxes that don’t really look much like shadows.
In this tutorial, we’ll learn how to transform typical box-shadows into beautiful, life-like ones:
Link to this heading
Modern 3D illustration tools like Blender can produce realistic shadows and lighting by using a technique known as raytracing.
In raytracing, hundreds of beams of lights are shot out from the camera, bouncing off of the surfaces in the scene hundreds of times. This is a computationally-expensive technique; it can take minutes to hours to produce a single image!
Web users don’t have that kind of patience, and so the box-shadow algorithm is much more rudimentary. It creates a box in the shape of our element, and applies a basic blurring algorithm to it.
As a result, our shadows will never look photo-realistic, but we can improve things quite a bit with a nifty technique: layering.
Instead of using a single box-shadow, we’ll stack a handful on top of each other, with slightly-different offsets and radiuses:
By layering multiple shadows, we create a bit of the subtlety present in real-life shadows.
This technique is described in detail in Tobias Ahlin’s wonderful blog post, “Smoother and Sharper Shadows with Layered box-shadow”.
Philipp Brumm created an awesome tool to help generate layered shadows: shadows.brumm.af:
Link to this heading
So far, all of our shadows have used a semi-transparent black color, like hsl(0deg 0% 0% / 0.4). This isn’t actually ideal.
When we layer black over our background color, it doesn’t just make it darker; it also desaturates it quite a bit.
Compare these two boxes:
The box on the left uses a transparent black. The box on the right matches the color’s hue and saturation, but lowers the lightness. We wind up with a much more vibrant box!
A similar effect happens when we use a darker color for our shadows:
To my eye, neither of these shadows is quite right. The one on the left is too desaturated, but the one on the right is not desaturated enough; it feels more like a glow than a shadow!
It can take some experimentation to find the Goldilocks color:
By matching the hue and lowering the saturation/lightness, we can create an authentic shadow that doesn’t have that “washed out” grey quality.
Link to this heading
Fitting into a design system
The shadows we’ve seen need to be customized depending on their elevation and environment. This might seem counter-productive, in a world with design systems and finite design tokens. Can we really “tokenize” these sorts of shadows?
We definitely can! Though it will require the assistance of some modern tooling.
For example, here’s how I’d solve this problem using React, styled-components, and CSS variables:
I have a static ELEVATIONS object, which defines 3 elevations. The color data for each shadow uses a CSS variable, --shadow-color.
Every time I change the background color (in Wrapper and BlueWrapper), I also change the --shadow-color. That way, any child that uses a shadow will automatically have this property inherited.
If you’re not experienced with CSS variables, this might seem like total magic. This is just meant as an example, though; feel free to structure things differently!
Earlier, I mentioned that my strategy for box shadows used to be “tinker with the values until it looks alright”. If I’m being honest, this was my approach for all of CSS. 😅
CSS is a tricky language because it’s implicit. I learned all about the properties, stuff like position and flex and overflow, but I didn’t know anything about the principles driving them, things like stacking contexts and hypothetical sizes and scroll containers.
In CSS, the properties are sorta like function parameters. They’re the inputs used by layout algorithms and other complex internal mechanisms.
A few years back, I decided to take the time to learn how CSS really works. I went down MDN rabbit holes, occasionally drilling down all the way to the solid core. And when I’d run into one of those dastardly situations where things just didn’t seem to make sense, I would settle into the problem, determined to poke at it until I understood what was happening.
This was not a quick or easy process, but by golly it was effective. All of a sudden, things started making so much more sense. CSS is a language that rewards those who go deep.
About a year ago, I started thinking that maybe my experience could help expedite that process for other devs. After all, most of us don’t have the time (or energy!) to spend years spelunking through docs and specs.
I left my job as a staff software engineer at Gatsby Inc., and for the past year, I’ve been focused full-time on building a CSS course unlike anything else out there.
There are over 200 lessons, spread across 10 modules. And you’ve already finished one of them: this tutorial on shadow design was adapted from the course! Though, in the course, there are also videos and exercises and minigames.
If you find CSS confusing or frustrating, I want to help change that. You can learn more at css-for-js.dev.
Throughout this tutorial, we’ve been using the box-shadow property. box-shadow is a great well-rounded tool, but it’s not our only shadow option in CSS. 😮
Take a look at filter: drop-shadow:
The syntax looks nearly identical, but the shadow it produces is different. This is because the filter property is actually a CSS hook into SVG filters. drop-shadow is using an SVG gaussian blur, which is a different blurring algorithm from the one box-shadow uses.
There are some other important differences between the two, but right now I wanna focus on drop-shadow‘s superpower: it contours the shape of the element.
For example, if we use it on an image with transparent and opaque pixels, the shadow will only apply to the opaque ones:
This works on images, but it also works on HTML elements! Check out how we can use it to apply a shadow to a tooltip that includes the tip:
(It’s subtle, since we’re using a soft shadow; try reducing the blur radiuses to see the contouring more clearly!)
One more quick tip: unlike box-shadow, the filter property is hardware-accelerated in Chrome, and possibly other browsers. This means that it’s managed by the GPU instead of the CPU. As a result, the performance is often much better, especially when animating. Just be sure to set will-change: transform to avoid some Safari glitch bugs.
I hope this tutorial inspired you to add or tweak some shadows! Honestly, very few developers put this level of thought into their shadows. And it means that most users aren’t used to seeing lush, realistic shadows. Our products stand out from the crowd when we put a bit more effort into our shadows.