Fernway Store
Curated Component Catalog
- Order no:
- #4821
- Date:
- Aug 23, 2026 · 10:42 AM
- Payment:
- Card ···· 4242
| Item | Price |
|---|---|
| Fernway Pro License | $200.00 |
| Animation BundleMicro-interactions & physics | $20.00 |
- Subtotal:
- $220.00
- Total:
- $220.00
* 4821 *
Choose your reading theme.
A thermal receipt feeds out of a black printer, with vibration, status lights and real controls.
receipt-printer/receipt-printer.tsxCurated Component Catalog
| Item | Price |
|---|---|
| Fernway Pro License | $200.00 |
| Animation BundleMicro-interactions & physics | $20.00 |
* 4821 *
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
When not to use
You can add this component with the shadcn CLI or by hand:
Into components/receipt-printer of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/receipt-printer.jsonCopy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
As shown under Usage, below.
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.
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.
A café’s order in euros with two coffees on one line, printed on cream paper through defaultPaper.
A stationery shop’s five lines in yen, a currency without decimals; the window grows to the receipt’s length.
A single item paid in cash; with printDuration at 0.8, Replay feeds it out in under a second.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
items | ReceiptItem[] | — | The receipt’s lines, in order: a name, a unit price, and an optional quantity and description. The subtotal and total are their sum. |
merchant | string | 'Fernway Store' | The shop’s name: in capitals on the receipt, and the first line of the printed copy. |
tagline | string | 'Curated Component Catalog' | The line under the shop’s name. |
orderNumber | string | '#4821' | Shown and copied as given, and used in announcements and the print title; the barcode’s caption drops its first “#”. |
paymentMethod | string | 'Card ···· 4242' | How it was paid, for display only: the component charges nothing. |
footnote | string | 'Auth #99824 · Fernway Ecosystem' | The small print at the foot of the receipt, in capitals. |
currency | string | 'USD' | A three-letter currency code; every amount is formatted in it. |
autoPrint | boolean | true | Print once it is seen (with autoplay off, it shows printed from the start); false leaves the window empty until Replay or Print. |
printDuration | number | 2.4 | How long a print takes, in seconds (at least 0.2). |
autoplay | boolean | true | Play 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. |
className | string | — | 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.
// 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 (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;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 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;