---
title: "The candy button: a glossy 3D CTA from four shadows"
description: "How to puff a flat blue rectangle into a tactile, pressable call-to-action using nothing but layered box-shadows — drop, ring, inset stroke, and a top sheen — plus hover and press states. Tailwind and plain-CSS versions."
date: "2026-05-29"
tags: ["css", "tailwind", "buttons", "frontend"]
---

Some buttons beg to be clicked. They look like a glossy candy or a physical key
— puffed up, catching light, casting a soft shadow. The surprise is there's no
3D and no images involved: it's **four box-shadows** stacked on a blue
rectangle.

Hover to float it, press to push it in. Here's every layer.

## Four shadows, one rectangle

Read these top-to-bottom — outermost effect first, then inward:

```css
box-shadow:
  0 3px 6px rgba(0, 0, 0, 0.2),             /* 1. drop  — elevation off the page */
  0 0 0 1px #296ff0,                         /* 3. ring  — seats the blue edge    */
  inset 0 0 0 1px rgba(255, 255, 255, 0.4),  /* stroke   — bright 1px rim          */
  inset 0 1px 6px 2px rgba(255, 255, 255, 0.24); /* 2. sheen — inner top gloss   */
```

- **Drop shadow** (`0 3px 6px`, black 20%) lifts it off the surface so it floats.
- **Blue ring** (`0 0 0 1px`, the fill blue) wraps the edge so the button reads
  as a solid object rather than a flat fill that was pasted on.
- **Inset white stroke** (`inset 0 0 0 1px`, white 40%) is the bright rim that
  edges the top and sides — the highlight where the "plastic" curves over.
- **Inset sheen** (`inset 0 1px 6px 2px`, white 24%) is the soft glow under the
  top edge. This is the single layer that turns "blue box" into "glossy candy."

That maps one-to-one to the Fill / Stroke / Drop / Inner / Drop layers you'd set
in a design tool — the browser does all of it in one property.

## States that feel physical

A 3D look has to react to touch or the illusion breaks.

```css
.btn-candy {
  --lift: 0px;
  transform: translateY(var(--lift));
  transition: transform .16s cubic-bezier(.2,.7,.2,1),
              box-shadow .16s ease, filter .16s ease;
}
/* rise + the drop shadow grows and tints blue → it floats */
.btn-candy:hover {
  --lift: -2px;
  filter: brightness(1.05);
  box-shadow: 0 8px 18px rgba(41,111,240,.35), 0 0 0 1px #296ff0,
              inset 0 0 0 1px rgba(255,255,255,.45),
              inset 0 1px 6px 2px rgba(255,255,255,.3);
}
/* sink + swap to an inset dark shadow → it presses in */
.btn-candy:active {
  transform: translateY(1px) scale(.99);
  filter: brightness(.97);
  box-shadow: 0 1px 3px rgba(0,0,0,.25), 0 0 0 1px #2563d8,
              inset 0 2px 6px rgba(0,0,0,.2);
}
```

Hover raises the button and grows a blue-tinted shadow, so it lifts toward you.
Active flips the logic: the elevation shadow collapses, an inset *dark* shadow
appears, and the whole thing scales down a hair — light now falls *into* it, the
universal signal for "pressed." A snappy `cubic-bezier(.2,.7,.2,1)` keeps the
motion crisp, and it all sits behind `prefers-reduced-motion`.

## Tailwind, if you prefer

Everything maps to utilities — the gradient and the long shadow lists go in
arbitrary values:

```tsx
  className="inline-flex items-center gap-2 rounded-[18px] px-8 py-4 text-lg
    font-semibold text-white bg-[linear-gradient(180deg,#3F7DF4,#296FF0)]
    shadow-[0_3px_6px_rgba(0,0,0,0.2),0_0_0_1px_#296FF0,inset_0_0_0_1px_rgba(255,255,255,0.4),inset_0_1px_6px_2px_rgba(255,255,255,0.24)]
    transition-[transform,box-shadow,filter] duration-150 ease-out
    hover:-translate-y-0.5 hover:brightness-105
    active:translate-y-px active:scale-[0.99]"
>
  <Plus className="size-5" /> Add team member
```

## Takeaways

- A "3D" button is just **layered shadows**: drop for lift, ring for edge, inset
  stroke for the rim, inset blur for the sheen.
- The inset white sheen is the highest-leverage layer — it's what makes it look
  glossy.
- Invert the shadow on `:active` (outer → inset, light → dark) and the press
  feels real.

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