Skip to content
← writing

A realistic hardware button in pure CSS

css
skeuomorphism
buttons
frontend

Flat design won, and then it got boring. Every so often a button comes along that looks like an actual object, machined metal and lit glass and a real press, and you remember screens can pretend to be physical. Here's one, built from three nested layers and a stack of gradients.

Press it. The glass sinks into the housing. No images, no SVG, four ideas.

1. The bezel is the outer element's padding

The metal frame isn't a border. It's the button's padding. A steep gray gradient plus a few inset bevels turns that band into machined metal lit from above:

.hw-btn {
  padding: 13px;                 /* this band IS the bezel */
  border-radius: 26px;
  background: linear-gradient(158deg, #5a5a62, #313137 13%, #1a1a1d 52%, #0a0a0b);
  box-shadow:
    inset 0 1.5px 0 rgba(255,255,255,.4),   /* bright top edge   */
    inset -1.5px 0 1px rgba(0,0,0,.55),     /* shadowed right    */
    inset 0 -2px 4px rgba(0,0,0,.7);        /* dark bottom inner */
}

A lit top edge over a dark bottom is the whole trick for metal. It tells your eye where the light is.

2. A black well, then convex glass

Don't sit the glass flush in the metal. Float it in a black recessed well. A middle layer with a near-black fill and a deep inset shadow gives you the dark margin all around the panel, so the glass reads as a separate part dropped into the housing:

.hw-btn__well {
  padding: 13px;                 /* the dark margin */
  border-radius: 20px;
  background: linear-gradient(180deg, #050506, #121214);
  box-shadow: inset 0 4px 9px rgba(0,0,0,.9);
}

The glass itself is a dark-to-bright-to-dark vertical gradient. That symmetry is what sells it as a convex tube lit through the middle, instead of a flat sticker:

.hw-btn__glass {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--hw-color) 72%, #000), var(--hw-color) 26%,
    color-mix(in srgb, var(--hw-color) 52%, #fff) 52%, var(--hw-color) 74%,
    color-mix(in srgb, var(--hw-color) 72%, #000));
}

3. A pseudo-element is the sheen

Real glass has a hard reflection on its upper curve. One gradient pseudo-element with an asymmetric border-radius fakes the bulge:

.hw-btn__glass::before {
  content: ""; position: absolute; inset: 1px 1px auto 1px; height: 58%;
  border-radius: 12px 12px 46% 46% / 12px 12px 30px 30px;
  background: linear-gradient(180deg, rgba(255,255,255,.62), rgba(255,255,255,.06));
}

That elliptical bottom radius is what curves the highlight instead of cutting it flat. It's the difference between glossy and glass. A second pseudo-element (::after) adds the soft, blurred waterline reflection you see across the lower third of real curved glass:

.hw-btn__glass::after {
  content: ""; position: absolute; left: 7%; right: 7%; bottom: 13%; height: 18%;
  border-radius: 100%; filter: blur(1.5px);
  background: linear-gradient(180deg, rgba(255,255,255,.5), transparent);
}

4. It emits light, and it presses

An outer box-shadow in the same hue spills a bloom onto the surface below, so the button looks like it's actually on. The press is physical too: drop it 2px and deepen the glass shadow so the light pushes inward.

.hw-btn {
  box-shadow: /* …bevels… */,
    0 10px 55px -6px color-mix(in srgb, var(--hw-color) 50%, transparent); /* bloom */
}
.hw-btn:active { transform: translateY(2px); }
.hw-btn:active .hw-btn__glass { box-shadow: inset 0 5px 10px rgba(0,0,0,.6); }

Most of this comes down to one habit: pick a light direction and commit. Lit top edge, dark bottom, panels recessed under the frame with an inset shadow, every shade mixed from one color-mix color. Do that and a few <div>s start to look like hardware.

Recolor it and flip the arrow on the Hardware Button page.

Ask your agent to implement this

Read the full writeup at https://seangeng.com/writing/a-realistic-hardware-button-in-css.md and implement it in my project.

It covers: A realistic hardware button in pure CSS. Recreating a physical, illuminated push-button in CSS: a gunmetal bezel, a recessed lit-glass panel, a glossy sheen, and a colored light bloom. Three nested layers, layered gradients, inset shadows. No images.

Requirements:
- Follow the technique/approach exactly as described in the writeup.
- Adapt names, colors, and styling to my project's existing conventions.
- If it's a component, make it reusable with sensible props and TypeScript types.
- Keep it accessible: semantic HTML, keyboard support, and respect prefers-reduced-motion.
- When done, tell me which files you created or changed and how to use it.

Paste into Claude Code, Codex, Cursor, or any agent. view raw .md download source .zip