← Buttons

3D Press Button

A chunky button with a visible edge underneath, like a key on an arcade machine. Hover it and it lifts a little; press it and it sinks down onto its edge as its shadow tightens; let go and it springs back up. Enter and Space press it too.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-press-button to a button. The script builds the face and edge for you.

Options

Add any of these attributes to the same element.

data-colorColour of the face. The edge is a darker shade of it. Default #6a0dad.
data-depthHow tall the edge is, in pixels. Default 8.
data-autoplayIt presses itself every couple of seconds while in view. Handy for demos.

Size it with font-size; style the face (padding, corners) with .prb-front.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // 3D Press Button: a chunky button with a visible edge underneath, like a
    // key on an arcade machine. Hover and it lifts a little; press and it
    // sinks down onto its edge, its shadow tightening; let go and it springs
    // back up. Enter and Space press it too.
    // Usage: <button data-press-button>Play</button>
    // Optional: data-color (face colour, default #6a0dad; the edge is a darker
    // shade of it), data-depth (how tall the edge is in px, default 8),
    // data-autoplay (it presses itself every couple of seconds while in view;
    // handy for demos).
    // Style the face with .prb-front (padding, radius, font). With reduced
    // motion it still moves down and up, just without the spring.
    var DEFAULTS = { color: '#6a0dad', depth: 8, every: 2000 };

    var css =
      '.prb{position:relative;display:inline-block;padding:0;border:0;background:none;cursor:pointer;font:inherit;' +
      'border-radius:14px;padding-top:var(--prb-d);-webkit-tap-highlight-color:transparent;outline-offset:4px}' +
      '.prb-shadow,.prb-edge{position:absolute;left:0;right:0;bottom:0;top:var(--prb-d);border-radius:inherit;pointer-events:none}' +
      '.prb-shadow{background:rgba(15,15,30,.28);filter:blur(4px)}' +
      '.prb-edge{background:linear-gradient(90deg,color-mix(in srgb,var(--prb-c) 55%,#000),color-mix(in srgb,var(--prb-c) 70%,#000) 10%,color-mix(in srgb,var(--prb-c) 70%,#000) 90%,color-mix(in srgb,var(--prb-c) 55%,#000))}' +
      '.prb-front{position:relative;display:block;padding:.8em 1.8em;border-radius:inherit;background:var(--prb-c);color:#fff;font-weight:700;' +
      'box-shadow:inset 0 1px 0 rgba(255,255,255,.25)}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var depth = +d.depth || DEFAULTS.depth;
      el.classList.add('prb');
      el.style.setProperty('--prb-c', d.color || DEFAULTS.color);
      el.style.setProperty('--prb-d', depth + 'px');

      var front = document.createElement('span');
      front.className = 'prb-front';
      while (el.firstChild) front.appendChild(el.firstChild);
      var shadow = document.createElement('span');
      shadow.className = 'prb-shadow';
      var edge = document.createElement('span');
      edge.className = 'prb-edge';
      shadow.setAttribute('aria-hidden', 'true');
      edge.setAttribute('aria-hidden', 'true');
      el.appendChild(shadow);
      el.appendChild(edge);
      el.appendChild(front);

      // The face rests a little above the edge's top, so the edge shows below
      // it; pressed, it sits almost flush.
      var REST = -depth * 0.5, HOVER = -depth * 0.75, DOWN = -depth * 0.08;
      var still = reduced();
      anime.utils.set(front, { y: REST });
      anime.utils.set(shadow, { y: depth * 0.3 });
      var pressed = false, hovered = false;

      function go(y, sh, dur, ease) {
        anime.animate(front, { y: y, duration: still ? 0 : dur, ease: ease });
        anime.animate(shadow, { y: sh, scaleX: y === DOWN ? 0.96 : 1, opacity: y === DOWN ? 0.6 : 1, duration: still ? 0 : dur, ease: ease });
      }
      function down() {
        if (pressed) return;
        pressed = true;
        go(DOWN, 0, 70, 'out(2)');
      }
      function up() {
        if (!pressed) return;
        pressed = false;
        go(hovered ? HOVER : REST, hovered ? depth * 0.55 : depth * 0.3, 520, 'outBack(2.6)');
      }
      function hover(on) {
        hovered = on;
        if (!pressed) go(on ? HOVER : REST, on ? depth * 0.55 : depth * 0.3, on ? 260 : 380, 'out(3)');
      }
      function press() { down(); setTimeout(up, 150); }

      el.addEventListener('pointerenter', function (e) { if (e.pointerType !== 'touch') hover(true); });
      el.addEventListener('pointerleave', function (e) { if (e.pointerType !== 'touch') hover(false); up(); });
      el.addEventListener('pointerdown', function (e) { if (e.button === 0) down(); });
      ['pointerup', 'pointercancel'].forEach(function (t) { el.addEventListener(t, up); });
      el.addEventListener('keydown', function (e) { if ((e.key === ' ' || e.key === 'Enter') && !e.repeat) down(); });
      el.addEventListener('keyup', function (e) { if (e.key === ' ' || e.key === 'Enter') up(); });
      el.addEventListener('blur', up);

      if (!still && d.autoplay !== undefined && d.autoplay !== 'false') {
        var timer = null;
        var tick = function () {
          if (!el.matches(':hover') && !pressed) press();
          timer = setTimeout(tick, DEFAULTS.every);
        };
        new IntersectionObserver(function (entries) {
          clearTimeout(timer);
          if (entries[0].isIntersecting) timer = setTimeout(tick, 600);
        }).observe(el);
      }
    }

    function init() {
      document.querySelectorAll('[data-press-button]').forEach(function (el) {
        if (el._prb) return;
        el._prb = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-press-button>Play now</button>

<!-- orange and extra deep: -->
<button data-press-button data-color="#f97316" data-depth="12">Extra deep</button>