# CSS box shadow: soft, layered shadows that look real

[Canonical HTML page](https://gradiently.design/guide/css-box-shadow)

A single grey box-shadow is the quickest way to make an interface look like a template. Real shadows are several shadows at once, tinted, and quieter than you think.

## The short version

- The CSS box shadow property takes an optional inset keyword, two offsets, a blur radius, a spread radius and a colour.
- A single box shadow looks artificial because real objects cast a sharp contact shadow and a wide soft one at the same time.
- Layering three to five shadows, each with roughly double the offset and blur of the last and a low opacity, produces a soft, believable shadow.
- Tinting the shadow towards the background's hue looks more natural than pure black on coloured or warm surfaces.
- Animating box-shadow repaints on every frame, so fade the opacity of a pseudo-element that carries the larger shadow instead.

A **CSS box shadow** is written `box-shadow: 0 4px 12px rgb(0 0 0 / 0.15)`: a horizontal offset, a vertical offset, a blur radius, an optional spread and a colour. That single shadow is fine for a quick prototype, but it rarely looks real. The shadows that convince the eye are layered: several shadows on one element, each larger and fainter than the last, tinted to suit the surface beneath.

## box-shadow syntax

```css
/* inset? | offset-x | offset-y | blur | spread | color */
.card { box-shadow: 0 4px 12px 0 rgb(0 0 0 / 0.15); }

/* Negative spread tucks the shadow under the box */
.card { box-shadow: 0 12px 24px -8px rgb(0 0 0 / 0.25); }

/* Inset: a shadow inside the box, like a pressed well */
.well { box-shadow: inset 0 2px 4px rgb(0 0 0 / 0.12); }

/* Spread only: a hairline that follows border-radius */
.chip { box-shadow: 0 0 0 1px rgb(15 23 42 / 0.08); }

/* Several shadows, comma separated, first on top */
.card { box-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 8px 16px rgb(0 0 0 / 0.08); }
```

The shadow always follows `border-radius`, which is why a spread only shadow makes a neat rounded hairline without changing layout.

| Value | Default | Effect |
| --- | --- | --- |
| `inset` | Off | Draws the shadow inside the border instead of outside |
| Offset x and y | Required | Moves the shadow. Light from above means a positive y |
| Blur | 0 | Softens the edge. Larger values spread the fade wider |
| Spread | 0 | Grows or shrinks the shadow before blurring. Negative tucks it in |
| Colour | `currentcolor` | Use an alpha colour; opaque shadows look like stickers |

Spread is the value most people skip and the one that fixes the most. A small negative spread stops a big blur from leaking out of the sides.

## Why a single shadow looks fake

Put a book on a table and look at its shadow. Right at the edge where it touches, the shadow is dark and crisp. Further out it becomes wide and faint, because light wraps around the object from many directions. One `box-shadow` can only be one of those things at a time: tight and hard, or wide and blurry. Either way the eye notices something is missing.

### Single shadow

- One medium blur in 25 percent black
- Grey halo all round the card
- Hard to tell how high the card floats
- Looks dirty on coloured backgrounds

### Layered shadow

- Four shadows, each fainter and wider
- Dense near the edge, airy far away
- Height reads at a glance
- Tinted to sit naturally on its surface

## How to layer box shadows

The method is simple: start small and sharp, then double the offset and blur for each extra layer, keeping every layer faint. The opacities add up where the layers overlap, so individual values can be surprisingly low.

1. **Start with a contact shadow** `0 1px 1px` at around 8 percent opacity. This is the dark line right under the edge.
2. **Double for each layer** Then `0 2px 2px`, `0 4px 4px`, `0 8px 8px`. Offset and blur grow together, so light seems to come from above.
3. **Keep each layer faint** Between 5 and 10 percent each. If the result looks heavy, remove the largest layer before turning anything darker.
4. **Tint the colour** Replace black with a deep version of the background hue, such as a navy on blue pages or a brown on cream.

```css
:root {
  /* Shadow colour as channels, so opacity can vary per layer */
  --shadow-color: 222 47% 11%;

  --shadow-sm:
    0 1px 1px hsl(var(--shadow-color) / 0.08),
    0 2px 2px hsl(var(--shadow-color) / 0.06);

  --shadow-md:
    0 1px 1px hsl(var(--shadow-color) / 0.08),
    0 2px 2px hsl(var(--shadow-color) / 0.07),
    0 4px 4px hsl(var(--shadow-color) / 0.06),
    0 8px 8px hsl(var(--shadow-color) / 0.05);

  --shadow-lg:
    0 1px 1px hsl(var(--shadow-color) / 0.07),
    0 2px 2px hsl(var(--shadow-color) / 0.07),
    0 4px 4px hsl(var(--shadow-color) / 0.06),
    0 8px 8px hsl(var(--shadow-color) / 0.06),
    0 16px 16px hsl(var(--shadow-color) / 0.05),
    0 32px 32px hsl(var(--shadow-color) / 0.04);
}

.card { box-shadow: var(--shadow-md); }
.dialog { box-shadow: var(--shadow-lg); }
```

A three step elevation scale as custom properties. Change `--shadow-color` once and every shadow in the system follows.

Storing shadows as variables turns them into design tokens, which keeps every card, menu and dialog consistent. [CSS variables for theming](https://gradiently.design/guide/css-custom-properties-theming) and [design tokens](https://gradiently.design/guide/design-tokens) show how to wire them into a full theme.

## An elevation scale for an interface

Shadow is a signal of height, so it should mean the same thing everywhere. Pick three or four levels and assign them to jobs. Anything that floats higher gets a bigger, softer shadow, never just a darker one.

| Level | Token | Use it for |
| --- | --- | --- |
| 0 | none, or a 1px spread hairline | Inputs, table rows, flat cards on a tinted page |
| 1 | `--shadow-sm` | Resting cards, buttons that should feel pressable |
| 2 | `--shadow-md` | Hovered cards, dropdown menus, popovers |
| 3 | `--shadow-lg` | Dialogs, sheets, anything over a dimmed page |

Four levels cover almost every interface. More than that and people stop being able to tell them apart.

## Shadows on coloured and dark backgrounds

Pure black shadows turn grey and muddy on a coloured surface, the same way black mixed into paint dulls it. Take the shadow colour from the background instead: a deep teal shadow on a mint page, a dark plum on a lilac one. On gradient backgrounds, pick the darkest stop of the gradient and use it at low opacity.

- Mint page: #f0fdfa
- Teal shadow: #134e4a
- Lilac page: #faf5ff
- Plum shadow: #3b0764
- Cream page: #fdf6ec
- Umber shadow: #451a03

Three page colours, each with the shadow colour that suits it. Use the shadow tones at 5 to 10 percent per layer.

In dark mode, shadows almost disappear, because there is little light for them to block. Show height by making raised surfaces slightly lighter instead, and keep a faint hairline at the edge. [Dark mode design](https://gradiently.design/guide/dark-mode-design) goes through the surface colours. For panels that float over imagery, a [backdrop filter](https://gradiently.design/guide/css-blur-background) often separates layers better than a heavier shadow.

## Animating box-shadow without jank

Changing `box-shadow` on hover forces the browser to repaint the shadow on every frame of the transition. On one button that is fine. On a grid of fifty cards it stutters. The standard fix is to put the larger shadow on a pseudo-element and fade its opacity, which the browser can composite cheaply.

```css
.card {
  position: relative;
  box-shadow: var(--shadow-sm);
}

.card::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transition: opacity 200ms ease-out;
  pointer-events: none;
}

.card:hover::after { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .card::after { transition: none; }
}
```

Both shadows are painted once. Hover only changes opacity, so nothing repaints during the transition.

More on what costs paint time is in [CSS gradient performance](https://gradiently.design/guide/css-gradient-performance), and the same layering approach suits buttons in [gradient buttons in CSS](https://gradiently.design/guide/css-gradient-button).

## When to skip the shadow

Shadows separate a surface from what is behind it. If the background is a rich gradient or photograph, a strong shadow fights it. Often a quieter background does more for the card than any shadow could. Gradiently's Marks are built for that: each one is a living background with its own colours, light and grain, and the words you place over it stay readable because the Mark calms itself behind them, with no boxes or shadows doing the work.

## FAQ

### What are the values in CSS box-shadow?

An optional `inset`, a horizontal offset, a vertical offset, a blur radius, a spread radius and a colour, for example `0 4px 12px 0 rgb(0 0 0 / 0.15)`.

### How do I make a soft box shadow in CSS?

Layer three to five shadows, doubling the offset and blur each time, at 5 to 10 percent opacity each. Tint them towards the background colour.

### How do I add more than one box shadow?

Separate them with commas in one `box-shadow` declaration. The first shadow listed is drawn on top.

### What does spread do in box-shadow?

It grows or shrinks the shadow before it is blurred. A negative spread keeps a large blur from showing at the sides of the box.

### Is animating box-shadow bad for performance?

It repaints every frame. Put the hover shadow on a pseudo-element and transition its opacity instead.
