712Tools
4 min read

CSS box-shadow patterns that look professional (not cheap)

A single 5px shadow makes any UI look like a bootstrapped 2015 landing page. Here are the layered-shadow patterns that modern design systems use โ€” Material, Tailwind, Apple.

By
Software Engineer ยท M.Sc. Mechanical Engineering ยท Ontario, Canada

Why single-shadow feels cheap

The default anyone writes:

box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);

That's a single blur, single color, single position. In real light, objects cast shadows with multiple falloffs at once โ€” a sharp near shadow and a soft ambient shadow. Simulating that means stacking two or three shadows.

Compare:

/* cheap */
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);

/* professional */
box-shadow:
  0 1px 2px rgba(0, 0, 0, 0.06),
  0 4px 6px rgba(0, 0, 0, 0.1),
  0 10px 15px rgba(0, 0, 0, 0.08);

Same element, dramatically different perceived polish.

The Material-inspired elevation scale

Google's Material Design (and every design system since) uses an elevation scale โ€” cards, dialogs, and buttons sit at different "heights" and cast different shadows.

Tailwind's default scale is a good starting point:

/* sm โ€” hairline shadow, subtle hover states */
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);

/* base โ€” the everywhere shadow, resting state */
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);

/* md โ€” cards, dropdown menus */
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);

/* lg โ€” popovers, tooltips */
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);

/* xl โ€” floating panels */
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);

/* 2xl โ€” dialogs, high-elevation cards */
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);

Use sm for hover states, md for resting cards, lg for floating UI. Ship all six as design tokens.

The five knobs

Every shadow has five parameters:

  1. X offset โ€” horizontal displacement. Positive = right.
  2. Y offset โ€” vertical. Positive = down. This is usually the only non-zero offset โ€” light comes from above.
  3. Blur radius โ€” how soft the edge is. Larger = softer.
  4. Spread โ€” expand or contract before blurring. Negative spread makes a tight, hard shadow.
  5. Color โ€” including alpha. RGBA at low opacity (0.05-0.15) is the default; solid black looks harsh.

The single most-underused knob is negative spread on the outer layer โ€” it tightens the shadow directly under the element, preventing the "floating in empty space" look.

Inset shadows for depth

The inset keyword paints the shadow inside the box:

box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);

Uses:

  • Form inputs โ€” inset shadows read as recessed, matching the "you can type here" affordance.
  • Pressed buttons โ€” combine :active { box-shadow: inset ... } with a slight downward transform.
  • Inner borders โ€” a 1px inset at low opacity is a nicer border than border: 1px solid.

Colored shadows

Recent design trend: shadows in the accent color, not black. A blue button casts a blue shadow, matching the object.

.btn-primary {
  background: #3b82f6;
  box-shadow: 0 8px 16px -4px rgba(59, 130, 246, 0.4);
}

Reads more expensive, more modern. Overdo it and the UI looks like a slot machine. Use sparingly โ€” big CTAs, hero cards.

Dark mode: shadows disappear

The single biggest shadow mistake: shipping the same shadow scale in dark mode. On a black background, a black shadow is invisible. On a dark-gray background, a black shadow is worse than invisible โ€” it flattens contrast.

Two approaches:

1. Skip shadows in dark mode. Use borders (border: 1px solid rgba(255,255,255,0.1)) instead. Cleaner, matches how macOS and iOS handle dark surfaces.

2. Use inset white glow. A subtle inset light-colored highlight on the top edge simulates ambient light.

@media (prefers-color-scheme: dark) {
  .card {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.05),
      0 8px 16px rgba(0, 0, 0, 0.4);
  }
}

Design systems that skip this get "invisible cards" complaints. Notion, Linear, and Figma all handle dark-mode shadows explicitly.

Performance: box-shadow is not free

Large blur radii are expensive to paint, and box-shadow triggers a repaint on every animation frame. Three optimizations:

  1. Cap the blur radius. Under 30px is fine; over 50px hurts on mid-range devices.
  2. Animate transform, not shadow. Transitioning box-shadow on :hover is choppy on 60fps displays. Transition transform: translateY(-2px) and keep the shadow static; the eye reads it as motion.
  3. Use filter: drop-shadow() for non-rectangular content. Icons, SVGs, images with transparency. box-shadow draws the shadow of the box; drop-shadow follows the alpha channel. Slower than box-shadow but the only option for irregular shapes.

Building shadows without eyeballing

Building shadows by tweaking numbers in DevTools is slow. Box Shadow Generator has sliders for every parameter, multi-layer stacking, and a live preview โ€” you build a professional shadow in seconds, not minutes.

The design-system checklist

For your own component library:

  1. Ship an elevation scale โ€” at least 4 levels (subtle, resting, floating, high).
  2. All shadows use CSS custom properties โ€” swap for dark mode centrally.
  3. Hover shadows are one step up the scale, not a random custom value.
  4. Pressed states use inset to communicate depth reversal.
  5. Test on a light and a dark background before shipping โ€” every scale value.

Related workflows

Tools mentioned in this post

Written by Shan

Shan builds 712 Tools. He holds a Master's degree in Mechanical Engineering and now works as a Software Engineer, shipping browser-based developer utilities out of Ontario, Canada. Learn more ยท 712studiogames@gmail.com