Pure CSS Buttons & forms #controls #faux-3d

Push button in pure CSS

Press it. The edge is a generated box-shadow stack that collapses on :active.

  • 44 lines of CSS
  • 3 lines of HTML
  • No JavaScript
  • No dependencies
  • Snippet under MIT
GitHub

How it works

  1. Every length is a multiple of one base unit, --u, tied to the canvas, so the cap, its type and the depth of its side are the same share of a gallery card, the editor and a recording canvas. The numbers below are those units.
  2. The "side" of the button is a stack of box-shadows going straight down, one every 4 units. Each copy is the whole cap, so they overlap into one solid slab.
  3. The <button> is a static, invisible hit target; the visible cap is a <span> inside it. A pressed element that moves itself can slide out from under the pointer.
  4. On :active, move the cap down with translateY and shrink the stack by the same amount — the base appears to stay put.
  5. Keep the transition very short (≈80ms) so it feels mechanical.
  6. A slight rotateX tilt sells the perspective.

Key techniques

  • box-shadow stack
  • :active on a static hit target
  • rotateX tilt

Copy-paste code

HTML 3 lines

<div class="scene">
  <button class="push" type="button"><span>PUSH</span></button>
</div>

CSS 44 lines

.scene {
  /* one base unit: every length below is a multiple of it, so the button is the same share of a
     card, the editor, a full screen and a recording canvas */
  --u: 0.28vmin;
  perspective: calc(600 * var(--u));
}

/* static hit target (its bottom padding covers where the cap travels to). It is lifted a little
   because the edge hangs below the cap: lifted, the solid button is centred. The drop shadow is
   kept tight so it adds little to what is drawn beyond what the eye sees */
.push {
  padding: 0 0 calc(32 * var(--u));
  border: 0;
  background: none;
  cursor: pointer;
  transform: translateY(calc(-10 * var(--u))) rotateX(30deg);
}

/* the visible cap */
.push span {
  display: block;
  padding: calc(56 * var(--u)) calc(22 * var(--u));
  border-radius: calc(20 * var(--u));
  font: 900 calc(26 * var(--u)) / 1.2 Inter, system-ui;
  letter-spacing: 0.14em;
  color: #fff;
  background: linear-gradient(#ff4d9d, #d63a80);
  pointer-events: none;
  box-shadow:
    0 calc(4 * var(--u)) 0 #8f2a58, 0 calc(8 * var(--u)) 0 #8f2a58,
    0 calc(12 * var(--u)) 0 #8f2a58, 0 calc(16 * var(--u)) 0 #8f2a58,
    0 calc(20 * var(--u)) 0 #8f2a58, 0 calc(24 * var(--u)) 0 #8f2a58,
    0 calc(28 * var(--u)) 0 #8f2a58, 0 calc(32 * var(--u)) 0 #8f2a58,
    0 calc(36 * var(--u)) 0 #8f2a58, 0 calc(40 * var(--u)) 0 #8f2a58,
    0 calc(48 * var(--u)) calc(24 * var(--u)) rgb(0 0 0 / 0.5);
  transition: transform 0.08s, box-shadow 0.08s;
}

.push:active span {
  transform: translateY(calc(32 * var(--u)));
  box-shadow:
    0 calc(4 * var(--u)) 0 #8f2a58, 0 calc(8 * var(--u)) 0 #8f2a58,
    0 calc(16 * var(--u)) calc(14 * var(--u)) rgb(0 0 0 / 0.5);
}
Standalone snippet · plain CSS, no build step, no dependencies.