---
title: "Buttons with real depth: stacked gradients + layered shadows"
description: "How a flat rectangle becomes a tactile, pressable button: two clipped gradients for fill and stroke, a three-part box-shadow, and hover/press states that actually move. Tailwind and plain-CSS versions."
date: "2026-01-24"
tags: ["css", "tailwind", "buttons", "frontend"]
---

A button is the most-clicked thing you ship, so it deserves more than
`bg-blue-500`. The good ones look like a physical key: a fill that catches
light, a hairline edge, a shadow that says press me. Here are two.

Hover to lift them, press to sink them. Three small tricks do the work.

## 1. Two gradients in one background

The fill and the edge are different gradients. CSS lets you paint both in a
single `background` by clipping each layer to a different box:

```css
background:
  linear-gradient(180deg, #201e25, #323137) padding-box,  /* fill   */
  linear-gradient(180deg, #4b4951, #313036) border-box;   /* stroke */
border: 1px solid transparent;
```

The first layer fills the padding box. The second fills all the way to the
border edge, and since the border is a transparent 1px, that gradient only ever
shows up *as* the border. You get a stroke that's brighter at the top, like
light hitting a bevel. It's the same Fill + Stroke split your design tool gives
you, in one declaration.

## 2. A three-part shadow

One shadow looks flat. Stack three and the button gets a body:

```css
box-shadow:
  0 2px 4px rgba(0, 0, 0, 0.1),           /* ambient: lifts off the page */
  0 0 0 1px #0d0d0d,                       /* ring: a crisp dark edge     */
  inset 0 1px 0 rgba(255, 255, 255, 0.06); /* sheen: top inner highlight  */
```

The ambient shadow handles elevation. The zero-blur `0 0 0 1px` ring sharpens
the silhouette. The `inset` highlight is the gloss along the top. The light
variant is the same recipe with the ring swapped for a translucent black and the
inner highlight pushed brighter.

## 3. States that move

Depth is a promise, and the button has to keep it when you touch it.

```css
.btn-gloss            { --lift: 0px; transform: translateY(var(--lift));
                        transition: transform .16s cubic-bezier(.2,.7,.2,1),
                                    box-shadow .16s ease, filter .16s ease; }
.btn-gloss:hover      { --lift: -1px; filter: brightness(1.08); }  /* rise   */
.btn-gloss:active     { transform: scale(.985); filter: brightness(.96); } /* sink */
.btn-gloss:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
```

Hover raises it a pixel and brightens it. Active scales it down a hair and dims
it. Together they read as a key being pressed. The `--lift` custom property
keeps the transform composable, and the `cubic-bezier(.2,.7,.2,1)` ease makes
the motion snap instead of drift. Wrap it in `prefers-reduced-motion` and you're
done.

## Tailwind, if you prefer

The shadows and states map straight to utilities. Only the dual-clip gradient
needs an inline `style`:

```tsx
  style={{
    background:
      "linear-gradient(180deg,#201E25,#323137) padding-box," +
      "linear-gradient(180deg,#4B4951,#313036) border-box",
  }}
  className="rounded-[18px] border border-transparent px-9 py-3.5 text-xl
    font-semibold text-zinc-100 transition-[transform,box-shadow,filter]
    duration-150 ease-out hover:-translate-y-px hover:brightness-110
    active:translate-y-0 active:scale-[0.985] active:brightness-95
    shadow-[0_2px_4px_rgba(0,0,0,0.1),0_0_0_1px_#0D0D0D,inset_0_1px_0_rgba(255,255,255,0.06)]"
>
  Accept
```

The whole thing is two clipped gradients, three stacked shadows, and a press
that moves. None of it is expensive, and it's the difference between a rectangle
and something that feels clickable.

Grab the prop-driven component on the
[Glossy Button](/components/gloss-button) page.
