Bruno DelorenceBruno
Appearance
Appearance

Choose your reading theme.

Contact

Receipt printer

New

A thermal receipt feeds out of a black printer, with vibration, status lights and real controls.

receipt-printer/receipt-printer.tsx
Live · interactive

Payment Complete

Receipt has been issued

Idle

POS-8000Standby

Fernway Store

Curated Component Catalog

Order no:
#4821
Date:
Aug 23, 2026 · 10:42 AM
Payment:
Card ···· 4242
ItemPrice
Fernway Pro License$200.00
Animation BundleMicro-interactions & physics$20.00
Subtotal:
$220.00
Total:
$220.00

* 4821 *

Thank you for your order!

Auth #99824 · Fernway Ecosystem

The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.

When to use

  • The moment right after a payment goes through, to confirm it with something to keep.
  • Checkouts with a short order: one to six lines, a total, an order number worth copying.
  • Point-of-sale, café and market products, where a till receipt is the familiar proof.
  • A confirmation page with room for a 360 px column that can take a couple of seconds to arrive.

When not to use

  • Invoices with tax lines, discounts or many items. Use a real invoice table, or a PDF.
  • Telling someone a payment failed or is pending: the banner only ever says it went through.
  • Inside a busy flow where waiting 2.4 s for the paper slows people down. Show the summary at once.
  • As the only record of a purchase. Send an email receipt too; this one isn’t saved anywhere.
  • A narrow side panel or a toast. Use a one-line confirmation with the order number instead.

You can add this component with the shadcn CLI or by hand:

Into components/receipt-printer of any React project, with no setup:

Terminal
npx shadcn@latest add https://brunodelorence.com/r/receipt-printer.json

Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.

Usage

order-confirmation.tsx
import { ReceiptPrinter, type ReceiptItem } from './receipt-printer';const items: ReceiptItem[] = [  { name: 'Annual subscription', price: 200 },  { name: 'Sticker pack', price: 4.5, quantity: 2, description: 'Matte vinyl' },];export function OrderConfirmation() {  return (    <ReceiptPrinter      merchant="Your store"      orderNumber="#4821"      items={items}      currency="EUR"      autoplay={false}      onPrintComplete={() => console.log('printed')}    />  );}

Each state below is a live instance, locked so you can study it.

The pieces it is built from, each with its measurements read live.

Below are variations of this component, each live, with its code.

Café order

A café’s order in euros with two coffees on one line, printed on cream paper through defaultPaper.

Long receipt

A stationery shop’s five lines in yen, a currency without decimals; the window grows to the receipt’s length.

Quick sale

A single item paid in cash; with printDuration at 0.8, Replay feeds it out in under a second.

On demand

With autoPrint off nothing prints by itself: the printer waits on Standby, its window empty, until Print or Replay feeds the receipt.

A payment confirmation becomes a physical receipt. The paper slides out of the printer’s slot while the body hums and its status lights pulse, then everything settles on Ready. Replay the print, copy the order number, change the paper or print a plain copy of the receipt.

  • The receipt is real text, laid out at rest inside a fixed-height window that clips it. A print is one CSS animation: the sheet slides down from 108% of its own height above its place while fading from 60% to 100%, on cubic-bezier(0.16, 1, 0.3, 1). Nothing reflows and the height never changes.
  • Each print remounts the sheet, so a replay starts again from the top and the old paper vanishes at once. The printer turns Ready when the animation ends.
  • While it feeds, the printer’s body jitters by under 0.6 px on a 180 ms cycle and both status lights pulse every 800 ms. With less than 15% of it on screen, or the tab hidden, all three pause where they are, so a print never finishes unseen.
  • The demo prints 400 ms after the component can play, then every 11 s, while it is visible and untouched; it waits while the reader uses it and never cuts a feed short. Automatic prints are silent.
  • Line totals are price × quantity, and every amount is formatted in the given currency. Copy puts the order number on the clipboard; Print opens the browser’s print dialog on a plain-text copy, built in a hidden frame that is removed afterwards.
  • The stage is light-dark() tokens, so it follows the page’s color-scheme, or the colorScheme prop: a pale backdrop by day, a near-black one at night, with the banner and the printer black on both. Untouched, the paper follows the scheme too and its swatch shows pressed; picking light, cream or dark paper holds through scheme changes.
PropTypeDefaultDescription
itemsReceiptItem[]—The receipt’s lines, in order: a name, a unit price, and an optional quantity and description. The subtotal and total are their sum.
merchantstring'Fernway Store'The shop’s name: in capitals on the receipt, and the first line of the printed copy.
taglinestring'Curated Component Catalog'The line under the shop’s name.
orderNumberstring'#4821'Shown and copied as given, and used in announcements and the print title; the barcode’s caption drops its first “#”.
paymentMethodstring'Card ···· 4242'How it was paid, for display only: the component charges nothing.
footnotestring'Auth #99824 · Fernway Ecosystem'The small print at the foot of the receipt, in capitals.
currencystring'USD'A three-letter currency code; every amount is formatted in it.
autoPrintbooleantruePrint once it is seen (with autoplay off, it shows printed from the start); false leaves the window empty until Replay or Print.
printDurationnumber2.4How long a print takes, in seconds (at least 0.2).
autoplaybooleantruePlay the demonstration (a first print, then one every 11 s) while visible and untouched; false keeps it still, the receipt printed.
onPrintComplete() => void—Called each time a feed finishes, and when Replay or Print prints at once under reduced motion.
defaultPaper'light' | 'cream' | 'dark'—The paper to begin with, as if picked on the swatches. Without it the paper follows the colour scheme.
defaultState'idle' | 'feeding' | 'printed'—Opens still in this state, with the demonstration off and autoPrint ignored: the window empty, the sheet held partway out with the lights on until Replay, or printed.
colorScheme'light' | 'dark' | 'system'—Its colours: light, dark, or the reader’s system setting. Without it the component follows the page’s color-scheme, which is light unless the page sets one.
classNamestring—Extra class for the root, the backdrop.

The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.

receipt-printer.tsx
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import {  useCallback,  useEffect,  useEffectEvent,  useLayoutEffect,  useMemo,  useRef,  useState,  type AnimationEvent,  type CSSProperties,  type RefObject,} from 'react';import { CopyIcon, PrinterIcon, ReplayIcon, TickIcon } from './icons';import { useAutoplay } from './use-autoplay';import styles from './receipt-printer.module.css';/* * Receipt printer: a payment confirmation printed as a till receipt. A dark banner says the * payment went through; under it a desktop thermal printer feeds the receipt out from beneath its * front lip, bottom edge first, buzzing while it prints, its lights pulsing until the paper
receipt-printer.module.css
/* Receipt printer (receipt-printer.tsx): a backdrop, light or near-black, holding a centred   column, at most 360px wide: the payment banner, the printer, the paper window, the buttons and   the paper swatches. The window is as tall as the receipt at rest and clips it, so the receipt   only ever moves inside it and the component keeps one height in every state. `data-phase` on   the root (idle, feeding, printed) drives the lights, the buzz and the feed; `data-paper` the   paper picked, on the swatches or by defaultPaper; `data-paused` freezes every animation while   the printer is out of sight.   The backdrop, the buttons, the swatches and the paper until one is picked are light-dark(light,   dark) tokens: the component takes the color-scheme it inherits from the page, or the one its   colorScheme prop sets (data-color-scheme). The banner and the printer are black objects on   both. */.root {  --sans: 'Library Inter', Inter, Arial, sans-serif;  --mono: 'Library Mono', ui-monospace, monospace;  --focus: #5c91ee;  --backdrop: #f8f8f8;  /* The buttons under the window, at rest and under the pointer. */  --button: #fff;  --button-edge: #e5e5e5;  --button-ink: #777;  --button-edge-hover: #bbb;  --button-ink-hover: #222;
icons.tsx
import type { ReactNode } from 'react';/* * The receipt printer's line icons, drawn on a 24-unit grid with a 1.5-unit round stroke in the * current text colour; the stylesheet sets their size. All decorative: the words beside them, or * the button around them, carry the meaning. */function Icon({ children }: { children: ReactNode }) {  return (    <svg      viewBox="0 0 24 24"      fill="none"      stroke="currentColor"      strokeWidth="1.5"      strokeLinecap="round"      strokeLinejoin="round"      aria-hidden="true"    >      {children}    </svg>  );}
use-autoplay.ts
'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) {  const ref = useRef<T>(null);  const [visible, setVisible] = useState(false);  const [reduced, setReduced] = useState(true);  const [taken, setTaken] = useState(false);  const timer = useRef(0);  useEffect(() => {    const element = ref.current;    if (!element) return;    let onScreen = false;    const update = () => setVisible(onScreen && !document.hidden);    const observer = new IntersectionObserver(      ([entry]) => {        onScreen = entry.isIntersecting;
  • The banner is a region named “Payment status”. The receipt is an article named for its order, with a heading, description lists for the order details and the totals, and a real table of items with column headers. Icons, the barcode, the notches and the serrated edge are hidden from assistive technology.
  • Replay, Copy order and Print are native buttons, followed by the light, cream and dark paper swatches (toggle buttons in a labelled group). While the paper feeds, Print is aria-disabled rather than disabled, so it keeps the focus.
  • One polite status region speaks only what the reader started: printing, ready with the total, copied or failed, or that print preview is unavailable.
  • Under reduced motion nothing moves or pulses and the receipt shows at rest; Replay and Print report it ready at once, and the demo never runs.