Receipt Print
A till receipt feeds out of a little printer line by line, in short jerky steps like the real thing, with the light on the printer glowing as it works. It ends with a torn zigzag edge. Loop it and the receipt is torn off and printed again.
ANIMACIOUS STORE
15 Nov 2026 · 14:32
Trail Runner£89.00
Sport socks ×2£12.00
Gift wrap£3.00
VAT£17.33
Total£104.00
Thank you!
How to use it
- Paste the snippet just before
</head>. - Add
data-receipt-printto an element with one child per line:<p>lines put their first and last parts at either edge, and<hr>is a dashed rule.
Options
Add any of these attributes to the same element.
data-speed | Time per line. Default 260 ms. |
data-loop | Tear it off and print again every few seconds while in view. |
It is 16em wide, so size it with font-size. The space for the whole receipt is kept from the start, so nothing around it jumps. It fires a printed event when done, and animacious.receiptPrint(element) prints it again.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Receipt Print: a till receipt feeds out of a little printer line by
// line, in short jerky steps like the real thing, ending in a torn
// zigzag edge. Each child of the element is one line of the receipt.
// Usage:
// <div data-receipt-print>
// <p><span>Trail Runner</span><span>£89.00</span></p>
// <hr>
// <p><b>Total</b><b>£89.00</b></p>
// </div>
// Optional: data-speed (ms per line, default 260), data-loop (tear it
// off and print again every few seconds while in view).
// Paragraphs are laid out with their first and last parts at either edge;
// <hr> is a dashed rule. Fires a "printed" event when done.
// Script use: animacious.receiptPrint(el) prints it again.
var DEFAULTS = { speed: 260, hold: 2400 };
var css =
'.rcp{position:relative;display:flex;flex-direction:column;align-items:center;width:16em;max-width:100%}' +
'.rcp-printer{position:relative;z-index:2;width:100%;height:1.9em;border-radius:.6em .6em .4em .4em;background:linear-gradient(#34344a,#1a1a2e);' +
'box-shadow:0 .3em .7em rgba(15,15,30,.25)}' +
'.rcp-printer::before{content:"";position:absolute;left:.55em;right:.55em;bottom:.35em;height:.3em;border-radius:999px;background:#0b0b14}' +
'.rcp-led{position:absolute;right:.75em;top:.45em;width:.42em;height:.42em;border-radius:50%;background:#3f3f55}' +
'.rcp-led.rcp-on{background:#4ade80;box-shadow:0 0 .4em #4ade80}' +
'.rcp-win{position:relative;width:calc(100% - 1.4em);margin-top:-.5em;overflow:hidden;height:0}' +
'.rcp-paper{position:absolute;left:0;right:0;bottom:0;filter:drop-shadow(0 .2em .25em rgba(15,15,30,.12))}' +
'.rcp-body{padding:1em .95em .5em;background:#fff;color:#1a1a2e;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:.8em;line-height:1.45;text-align:left}' +
'.rcp-body>*{margin:0}' +
'.rcp-body>p{display:flex;justify-content:space-between;gap:1em}' +
'.rcp-body hr{border:0;border-top:1.5px dashed #b8b8c8;margin:.45em 0}' +
'.rcp-teeth{height:.55em;background:linear-gradient(135deg,#fff 50%,transparent 50%) 0 0/.8em 100%,linear-gradient(-135deg,#fff 50%,transparent 50%) 0 0/.8em 100%}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function setup(el) {
el.classList.add('rcp');
var body = document.createElement('div');
body.className = 'rcp-body';
while (el.firstChild) body.appendChild(el.firstChild);
var printer = document.createElement('div');
printer.className = 'rcp-printer';
printer.setAttribute('aria-hidden', 'true');
printer.innerHTML = '<span class="rcp-led"></span>';
var win = document.createElement('div');
win.className = 'rcp-win';
var paper = document.createElement('div');
paper.className = 'rcp-paper';
var teeth = document.createElement('div');
teeth.className = 'rcp-teeth';
teeth.setAttribute('aria-hidden', 'true');
paper.appendChild(body);
paper.appendChild(teeth);
win.appendChild(paper);
el.appendChild(printer);
el.appendChild(win);
var s = el._rcp = { printer: printer, led: printer.firstChild, win: win, paper: paper, body: body, busy: false, visible: false };
// Reserve the full height up front, so nothing around it jumps.
var reserve = function () { el.style.minHeight = (printer.offsetHeight + paper.offsetHeight - 0.5 * parseFloat(getComputedStyle(el).fontSize) + 6) + 'px'; };
reserve();
new ResizeObserver(function () {
reserve();
if (!s.busy && s.done) win.style.height = paper.offsetHeight + 'px';
}).observe(paper);
if (reduced()) { win.style.height = paper.offsetHeight + 'px'; s.done = true; return; }
new IntersectionObserver(function (entries) {
s.visible = entries[0].isIntersecting;
if (s.visible && !s.busy && (!s.played || s.waiting)) { s.waiting = false; play(el); }
}, { threshold: 0.3 }).observe(el);
}
function play(el) {
var s = el._rcp, d = el.dataset;
if (s.busy) return;
s.busy = true;
s.played = true;
s.done = false;
var speed = +d.speed || DEFAULTS.speed;
// The paper hangs down from the slot, so the bottom of the receipt comes
// out first and each new line pushes the ones before it down. These are
// how much paper is out once each line (last to first) is printed.
var full = s.paper.offsetHeight, kids = s.body.children;
var stops = [full - s.body.offsetHeight + parseFloat(getComputedStyle(s.body).paddingBottom) * 0.5];
for (var i = kids.length - 1; i >= 0; i--) {
stops.push(full - kids[i].offsetTop + parseFloat(getComputedStyle(kids[i]).marginTop));
}
stops.push(full);
anime.utils.set(s.win, { height: 0, y: 0, rotate: 0, opacity: 1 });
s.led.classList.add('rcp-on');
var tl = anime.createTimeline({
onComplete: function () {
s.busy = false;
s.done = true;
s.led.classList.remove('rcp-on');
el.dispatchEvent(new CustomEvent('printed', { bubbles: true }));
if (on(d.loop)) setTimeout(function () { tear(el); }, DEFAULTS.hold);
}
});
var t = 200;
stops.forEach(function (h, i) {
// A quick feed, a tiny judder of the printer, then a pause while it "prints".
var feed = Math.max(80, speed * 0.45);
tl.add(s.win, { height: h, duration: feed, ease: 'out(2)' }, t)
.add(s.printer, { y: [0, 1, 0], duration: feed, ease: 'inOut(2)' }, t);
t += feed + (i === stops.length - 1 ? 0 : speed * 0.55);
});
}
// Tear the receipt off: it drops away and the printer starts again.
function tear(el) {
var s = el._rcp;
if (!s.visible) { s.waiting = true; return; }
s.busy = true;
anime.animate(s.win, {
y: [0, 40], rotate: [0, -4], opacity: [1, 0], duration: 600, ease: 'in(2)',
onComplete: function () {
s.busy = false;
setTimeout(function () { play(el); }, 500);
}
});
}
window.animacious = window.animacious || {};
window.animacious.receiptPrint = function (el) { if (el && el._rcp) play(el); };
function init() {
document.querySelectorAll('[data-receipt-print]').forEach(function (el) {
if (el._rcp) return;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-receipt-print>
<p><b>ANIMACIOUS STORE</b></p>
<hr>
<p><span>Trail Runner</span><span>£89.00</span></p>
<p><span>Sport socks ×2</span><span>£12.00</span></p>
<hr>
<p><b>Total</b><b>£101.00</b></p>
</div>