Pure CSS Buttons & forms #hover #faux-3d #sass-loop

Push button in pure CSS

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

  • 38 lines of CSS
  • 3 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed
GitHub

How it works

  1. The "side" of the button is a stack of 1px box-shadows going straight down.
  2. 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.
  3. On :active, move the cap down with translateY and shrink the stack by the same amount — the base appears to stay put.
  4. Keep the transition very short (≈80ms) so it feels mechanical.
  5. 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 38 lines

.scene {
  perspective: 600px;
}

/* static hit target (its bottom padding covers where the cap travels to) */
.push {
  padding: 0 0 8px;
  border: 0;
  background: none;
  cursor: pointer;
  transform: rotateX(30deg);
}

/* the visible cap */
.push span {
  display: block;
  padding: 18px 46px;
  border-radius: 16px;
  font: 900 1.4rem system-ui;
  letter-spacing: 0.14em;
  color: #fff;
  background: linear-gradient(#ff4d9d, #d63a80);
  pointer-events: none;
  box-shadow:
    0 1px 0 #8f2a58, 0 2px 0 #8f2a58, 0 3px 0 #8f2a58,
    0 4px 0 #8f2a58, 0 5px 0 #8f2a58, 0 6px 0 #8f2a58,
    0 7px 0 #8f2a58, 0 8px 0 #8f2a58, 0 9px 0 #8f2a58,
    0 10px 0 #8f2a58,
    0 18px 22px rgb(0 0 0 / 0.55);
  transition: transform 0.08s, box-shadow 0.08s;
}

.push:active span {
  transform: translateY(8px);
  box-shadow:
    0 1px 0 #8f2a58, 0 2px 0 #8f2a58,
    0 6px 10px rgb(0 0 0 / 0.55);
}

Sass source 52 lines

@use 'sass:list';

// A solid edge made from $depth stacked 1px shadows, plus one soft drop shadow.
@function edge($depth, $color) {
  $shadows: ();

  @for $i from 1 through $depth {
    $shadows: list.append($shadows, 0 #{$i}px 0 $color, comma);
  }

  @return list.append($shadows, 0 #{$depth + 8}px 22px rgb(0 0 0 / 0.55), comma);
}

$edge: color-mix(in srgb, var(--hot) 55%, #000);
$travel: 9px;

// The <button> is a static, invisible hit target; the visible cap is the <span> inside it.
// A pressed element that moves itself can slide out from under the pointer.
.d-button {
  padding: 0 0 $travel; // the hit area also covers where the cap travels to
  border: 0;
  background: none;
  cursor: pointer;
  transform-style: preserve-3d;
  transform: rotateX(32deg);

  span {
    display: block;
    padding: 18px 44px;
    border-radius: 16px;
    background: linear-gradient(var(--hot), color-mix(in srgb, var(--hot) 80%, #000));
    color: #fff;
    font-size: 22px;
    font-weight: 900;
    letter-spacing: 0.14em;
    pointer-events: none;
    box-shadow: edge(12, $edge);
    transition:
      transform 0.08s,
      box-shadow 0.08s,
      filter 0.2s;
  }

  &:hover span {
    filter: brightness(1.1);
  }

  &:active span {
    transform: translateY($travel);
    box-shadow: edge(3, $edge);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.