Shadows, Not Borders

A 1px border is the default way to give a card an edge. It works, but every border is a line, and a screen full of lines starts to look like a spreadsheet. A layered shadow draws the same edge with a faint ring, then adds a hint of depth below it. Same card, two treatments.

Border
Shadow

The shadow version is not softer or blurrier, it is still sharp at the edge. The difference is that the card now sits slightly above the page instead of being drawn on it. That is what lets you stack surfaces, like a card inside a panel inside a page, without the lines piling up.

Three layers

The recipe this site uses has three parts, and each does one job. Step through them.

  • The ring is a 0 0 0 1px shadow at 6% black. This is the edge. It replaces the border, but because it is a shadow it does not take up space or change the box size.
  • The contact layer is a tight, 1px offset shadow at 6%. It darkens the pixels right under the bottom edge, which is where a real object would touch the surface.
  • The ambient layer is a wider, 4px blur at 4%. It suggests light from above without ever becoming a visible smudge.

Several faint shadows look like one real shadow. One strong shadow looks like a drop shadow filter.

None of the layers is above 6% opacity. If you can consciously see the shadow, it is too strong.

Elevation

Once the base is built from layers, elevation is just adding more of them. A card at rest has a ring and a contact shadow. On hover it picks up a mid blur. A popover adds a long soft one on top of that.

Card
Hover
Popover

Keep the first two layers the same across every level. That is what makes the levels read as the same material at different heights, instead of three different styles.

Dark mode

Shadows are dark, and a dark shadow on a dark background is invisible. Here is the light mode shadow dropped onto a dark surface, next to the version this site actually uses in dark mode.

Wrong
Right

In dark mode the edge comes from light, not dark. A 1px inset ring at 3% white draws the outline from the inside, and a 1px inset highlight along the top edge suggests light hitting the surface. The outer shadows stay, but they are there for separation from what is below, not for the edge.

Usage

These are the real token values from this site's globals.css. The Tailwind tab uses the token directly, the CSS tab shows what is inside it.

app.tsx
<div class="rounded-xl bg-card shadow-(--custom-shadow)">
  <!-- content -->
</div>
:root {
  --custom-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.06),
    0 1px 2px -1px rgba(0, 0, 0, 0.06),
    0 2px 4px 0 rgba(0, 0, 0, 0.04);
}

.dark {
  --custom-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.03),
    inset 0 0 0 1px rgba(255, 255, 255, 0.03),
    0 0 0 1px rgba(0, 0, 0, 0.1),
    0 2px 2px 0 rgba(0, 0, 0, 0.1),
    0 4px 4px 0 rgba(0, 0, 0, 0.1),
    0 8px 8px 0 rgba(0, 0, 0, 0.1);
}

.card {
  border-radius: 12px;
  box-shadow: var(--custom-shadow);
}

Borders still have a place. Inputs, table rows, and dividers want a plain line, because they are not surfaces and should not look raised. Save the shadow for things that contain other things.

Resources

Search concepts

Search for a command to run...