Letterpress Press
A raised plate of type drops onto the page, thuds in and lifts away, leaving the words pressed into the paper: shaded along their top edges, with the light catching the paper just below, like a letterpress print.
Pressed in
How to use it
- Paste the snippet just before
</head>. - Add
data-letterpress-pressto a heading on a light, paper-coloured background.
Options
Add any of these attributes to the same element.
data-ink | Ink colour. Default #3d2c5a. |
data-depth | How deep the impression looks: 1.5 is half as deep again. Default 1. |
data-loop | Fade the print away and press it again, forever. |
data-hold | How long the print stays before fading when looping. Default 2400 ms. |
data-delay | Wait before the first press. Default 200 ms. |
Click or tap the text to press it again. The inset look needs a light background: on a dark one, use a light ink and a smaller data-depth.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Letterpress Press: a raised plate of type comes down onto the page,
// presses in with a small thud and lifts away, leaving the words debossed
// into the paper with an inset shadow, like letterpress printing. Click or
// tap to press it again.
// Usage: <h1 data-letterpress-press>Pressed in</h1>
// Optional: data-ink (ink colour; default #3d2c5a), data-depth (how deep
// the impression looks, 1 = normal; default 1), data-loop (fade out and
// press again, forever), data-hold (ms the impression stays when looping;
// default 2400), data-delay (ms before the first press; default 200).
var DEFAULTS = { ink: '#3d2c5a', depth: 1, hold: 2400, delay: 200 };
var css =
'.lpp-wrap{position:relative;display:inline-block}' +
'.lpp-ink{display:block;color:transparent;-webkit-text-fill-color:transparent;-webkit-background-clip:text;background-clip:text}' +
'.lpp-plate{position:absolute;inset:0;pointer-events:none;opacity:0;transform-origin:50% 50%;will-change:transform,opacity}';
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 ink = d.ink || DEFAULTS.ink;
var depth = d.depth !== undefined ? +d.depth : DEFAULTS.depth;
var loop = d.loop !== undefined && d.loop !== 'false';
var hold = +d.hold || DEFAULTS.hold;
var delay = d.delay !== undefined ? +d.delay : DEFAULTS.delay;
var wrap = document.createElement('span');
wrap.className = 'lpp-wrap';
var impression = document.createElement('span');
impression.className = 'lpp-ink';
while (el.firstChild) impression.appendChild(el.firstChild);
impression.style.backgroundColor = ink;
var plate = document.createElement('span');
plate.className = 'lpp-plate';
plate.setAttribute('aria-hidden', 'true');
plate.textContent = impression.textContent;
plate.style.color = ink;
wrap.appendChild(impression);
wrap.appendChild(plate);
el.appendChild(wrap);
// s: 0 = flat ink, 1 = fully pressed in. The light falls from above, so
// the top edge of each letter is in shade and the paper below it catches light.
var st = { s: 1, ink: 1, lift: 1 };
function drawImpression() {
var k = st.s * depth;
impression.style.textShadow =
'0 ' + (0.035 * k).toFixed(4) + 'em ' + (0.025 * k).toFixed(4) + 'em rgba(255,255,255,' + (0.38 * Math.min(1, k)).toFixed(3) + '),' +
'0 ' + (-0.012 * k).toFixed(4) + 'em ' + (0.01 * k).toFixed(4) + 'em rgba(0,0,0,' + (0.22 * Math.min(1, k)).toFixed(3) + ')';
impression.style.opacity = st.ink.toFixed(3);
}
// lift: 1 = hovering high above the page, 0 = touching it.
function drawPlate() {
var h = st.lift;
plate.style.transform = 'translateY(' + (-0.12 * h).toFixed(4) + 'em) scale(' + (1 + 0.12 * h).toFixed(4) + ')';
plate.style.textShadow = '0 ' + (0.02 + 0.3 * h).toFixed(4) + 'em ' + (0.02 + 0.25 * h).toFixed(4) + 'em rgba(20,10,30,' + (0.4 - 0.22 * h).toFixed(3) + ')';
}
drawImpression();
if (reduced()) return;
var running = false, visible = false, waiting = false;
function press() {
running = true;
st.s = 0; st.ink = 0; st.lift = 1;
drawImpression(); drawPlate();
var tl = anime.createTimeline({ onComplete: after });
// The plate falls onto the page...
tl.add(plate, { opacity: [0, 1], duration: 260, ease: 'out(2)' }, 0)
.add(st, { lift: 0, duration: 620, ease: 'in(3)', onUpdate: drawPlate }, 0)
// ...thuds in, the paper giving a little...
.add(wrap, { scale: [{ to: 0.975, duration: 90, ease: 'out(2)' }, { to: 1, duration: 420, ease: 'outElastic(1, .5)' }] }, 620)
.add(st, { ink: 1, duration: 60, ease: 'linear', onUpdate: drawImpression }, 620)
.add(st, { s: 1, duration: 520, ease: 'out(3)', onUpdate: drawImpression }, 640)
// ...and lifts away, leaving the impression.
.add(st, { lift: 0.7, duration: 560, ease: 'out(2)', onUpdate: drawPlate }, 760)
.add(plate, { opacity: 0, duration: 420, ease: 'out(2)' }, 760);
}
function after() {
if (!loop) { running = false; return; }
setTimeout(function () {
// The page is swapped for a fresh sheet.
anime.animate(st, {
ink: 0, duration: 600, ease: 'inOut(2)', onUpdate: drawImpression,
onComplete: function () {
setTimeout(function () {
running = false;
if (visible) press(); else waiting = true;
}, 350);
}
});
}, hold);
}
el.addEventListener('pointerdown', function () { if (!running) press(); });
st.ink = 0; drawImpression();
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible && !running && (waiting || !el._lppPlayed)) {
waiting = false;
el._lppPlayed = true;
running = true;
setTimeout(press, delay);
}
}, { threshold: 0.2 }).observe(el);
}
function init() {
document.querySelectorAll('[data-letterpress-press]').forEach(function (el) {
if (el._lpp) return;
el._lpp = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><h1 data-letterpress-press>Pressed in</h1>
<!-- deep red ink, pressed harder, on a loop: -->
<h1 data-letterpress-press data-ink="#9b2c2c" data-depth="1.5" data-loop>Pressed in</h1>