Receipt

No dependencies
Code format
React + Tailwind
import { useId } from 'react';

const css = `
  /* The slip feeds out a line at a time, waits, gets a tug and is torn away to the left. */
  @keyframes loader-printer-feed {
    0% { transform: translateY(51px) rotate(0deg); opacity: 1; animation-timing-function: steps(12, end); }
    58% { transform: translateY(0) rotate(0deg); opacity: 1; }
    70% { transform: translateY(0) rotate(0deg); opacity: 1; animation-timing-function: ease-out; }
    74% { transform: translateY(-3px) rotate(2deg); opacity: 1; animation-timing-function: ease-in; }
    88% { transform: translate(-16px, -14px) rotate(-22deg); opacity: 0; }
    88.1%, 100% { transform: translateY(51px) rotate(0deg); opacity: 0; }
  }
  @keyframes loader-printer-led { 0%, 56% { opacity: 1; } 28% { opacity: 0.35; } 60%, 100% { opacity: 0.2; } }
  .loader-printer { position: relative; width: 104px; height: 113px; }
  .loader-printer svg { width: 100%; height: 100%; overflow: visible; }
  .loader-printer__slip { transform-origin: 45px 57px; animation: loader-printer-feed 3s infinite; }
  /* Paper stays pale in both themes and the ink dark on it. */
  .loader-printer__paper { fill: light-dark(#ebedf0, #e5e6e9); stroke: light-dark(#cbced3, #e5e6e9); stroke-width: 0.8; }
  .loader-printer__ink { stroke: light-dark(#6d7179, #393c41); stroke-width: 2; stroke-linecap: round; }
  .loader-printer__ink--thin { stroke-width: 1; stroke-dasharray: 2 2; stroke-linecap: butt; }
  .loader-printer__ink--bold { stroke-width: 3; }
  .loader-printer__top { fill: light-dark(#cbced3, #393c41); }
  .loader-printer__body { fill: light-dark(#dbdde1, #2e3035); stroke: light-dark(#6d7179, #7d8087); stroke-width: 1.2; }
  .loader-printer__slot { fill: light-dark(#fbfcfd, #121417); }
  .loader-printer__led { fill: light-dark(#373a40, #f1f2f3); animation: loader-printer-led 3s steps(1, end) infinite; }
  .loader-printer__key { fill: light-dark(#cbced3, #393c41); }
  .loader-printer__grille { stroke: light-dark(#cbced3, #393c41); stroke-width: 1.5; stroke-linecap: round; }
  @media (prefers-reduced-motion: reduce) {
    .loader-printer__slip, .loader-printer__led { animation: none; }
  }
`;

// A torn top edge: the last slip was ripped off here.
const TEAR = Array.from({ length: 15 }, (_, i) => `${24 + i * 3} ${i % 2 ? 6 : 9}`).join('L');

export function PrinterLoader() {
  const clip = `loader-printer-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
  return (
    <>
      <style>
        {css}
      </style>
      <div className="loader-printer" role="status" aria-label="Loading">
        <svg viewBox="0 0 92 100" aria-hidden="true">
          <defs>
            <clipPath id={clip}>
              <rect x="-40" y="-40" width="172" height="97.5" />
            </clipPath>
          </defs>
          <g clipPath={`url(#${clip})`}>
            <g className="loader-printer__slip">
              <path className="loader-printer__paper" d={`M${TEAR}L66 9V57H24Z`} />
              <path className="loader-printer__ink loader-printer__ink--bold" d="M38 15H52" />
              <path className="loader-printer__ink" d="M29 22H44M54 22H61M29 27H40M54 27H61M29 32H46M54 32H61M29 37H38M54 37H61" />
              <path className="loader-printer__ink loader-printer__ink--thin" d="M28 42H62" />
              <path className="loader-printer__ink loader-printer__ink--bold" d="M29 48H40M52 48H61" />
            </g>
          </g>
          <rect className="loader-printer__body" x="8" y="58" width="76" height="36" rx="8" />
          <rect className="loader-printer__top" x="12" y="54" width="68" height="8" rx="3" />
          <rect className="loader-printer__slot" x="20" y="56" width="52" height="3" rx="1.5" />
          <path className="loader-printer__grille" d="M18 72H40M18 77H40M18 82H40" />
          <circle className="loader-printer__led" cx="70" cy="72" r="2.2" />
          <rect className="loader-printer__key" x="58" y="79" width="16" height="6" rx="3" />
        </svg>
      </div>
    </>
  );
}

Free to use in any project, commercial included. MIT-0, no attribution required. Licence

Install with shadcn

npx shadcn@latest add @movin/loader-printer
Source on GitHub

About this component

A receipt printer feeding out a slip line by line while its light blinks, then the slip is torn off and flutters away for the next one.

Dependencies
None
Difficulty
Intermediate
Triggers
continuous, loop
Techniques
keyframes, clip path, svg
Reduced motion
Respected
Theme
Light and dark (follows your page's color-scheme)

The root has role="status" and aria-label="Loading"; everything inside is decorative. Under prefers-reduced-motion the printer is still.

More loaders

See all
Snail
Toaster
Wiper
Yo-yo