Design2026-07-21
How I Build Consistent Shadow Systems For My UI
A practical guide to building a consistent CSS box-shadow system with three elevation levels, three-layer shadows, and color-aware techniques for light and dark mode.
I used to add shadows by feel. I'd open the dev tools, tweak the X and Y offsets until something looked "right," and move on. Every element had its own custom shadow, determined entirely by my mood at that moment. The result was predictable: a chaotic mess of conflicting elevations that made my interfaces look like a paper-shredding accident.
A shadow system isn't about making things "pop." It's about creating a believable elevation hierarchy. When every element floats at a different height with no consistent logic, your brain knows something is off — even if you can't articulate why. That subtle discomfort erodes trust in the interface.
The turning point for me was when I forced myself to define exactly three elevation levels and stick to them no matter what. Level one was for rest state elements that sit directly on the surface — cards, modals resting, dropdown menus in their natural position. Level two was for elements that lift on interaction — buttons on hover, cards on hover, tooltips appearing. Level three was for elements that demand attention — modals open, dialogs, notifications.
Here's the actual math I use now. Each shadow has three layers: a large, subtle shadow for depth, a medium shadow for definition, and a tight shadow for edge sharpness. The large shadow uses a wide blur radius with low opacity (like 0.05). The medium shadow uses a moderate blur and slightly higher opacity (0.08). The tight shadow uses a small blur with the highest opacity (0.12). This three-layer approach creates what I call "believable shadow" — it mimics how light behaves in the real world, where objects cast both ambient occlusion and directional shadows.
I start with a base shadow for level zero (none). Level one gets large shadow at 12px blur, medium at 6px, tight at 1.5px. Level two scales those up — 24px large, 10px medium, 2.5px tight. Level three goes to 48px large, 18px medium, 4px tight. The multipliers aren't random — they follow a 2x progression, which keeps the relationship between levels predictable.
Color matters more than most people realize. Pure black shadows always look muddy and fake. I use the element's own background color with black mixed in, or better yet, I use a colored shadow that matches the dominant accent color of the UI. For dark mode, I actually use lighter shadows with very low opacity — the illusion of light from below, which feels more natural on a dark surface.
Once I had this system, I built it into a small CSS custom property set. Every shadow in my entire design system is now controlled by exactly nine variables: three for each elevation, plus a spread and color override per elevation. Changing the entire feel of the interface is as simple as adjusting the color variable.
There's a tool in the reference section here called the Shadow Palette Generator that lets you define your base values and spits out the full system. It handles the math, generates the CSS, and even previews each elevation on your chosen background color. I use it every time I start a new project now.
The biggest win wasn't even visual consistency — it was speed. When I need to decide what shadow a new component gets, there's no deliberation. It's a direct mapping from its role in the interface to its elevation level. Design review conversations went from "that shadow feels off" to "this component should be level two, we need to change its role." That shift from subjective feedback to objective system rules is what makes a design system actually work.
Start small. Pick three elevations. Write them down. Enforce them for two weeks. You'll never go back to shadow-by-vibes again.