← E-commerce

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

  1. Paste the snippet just before </head>.
  2. Add data-receipt-print to 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-speedTime per line. Default 260 ms.
data-loopTear 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.

1 · Snippet
<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>
2 · Your HTML
<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>