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
- Paste the snippet just before
</head>. - Add
data-press-buttonto a button. The script builds the face and edge for you.
Options
Add any of these attributes to the same element.
data-color | Colour of the face. The edge is a darker shade of it. Default #6a0dad. |
data-depth | How tall the edge is, in pixels. Default 8. |
data-autoplay | It 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.
<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><button data-press-button>Play now</button>
<!-- orange and extra deep: -->
<button data-press-button data-color="#f97316" data-depth="12">Extra deep</button>