Receipt Printer
A receipt that prints after checkout like a thermal printer feeding paper line by line: processing, printing and complete stages, a serrated paper edge, tear-off and replay.
This is a WebberUI Pro component
The install command below needs a licence to run; without one it returns 401.
The moment a payment succeeds, let the receipt come out notch by notch like a thermal receipt printer. The component has three stages: while processing, the indicator light on the printer body pulses and the small screen shows the status; while printing, the paper feeds out of the slot in 20 stepped jolts (or in one smooth motion); once complete, the receipt can be torn off with a button or by dragging it down, and it drops away with a random tilt. The serrated bottom edge is generated with clip-path and the tooth count is adjustable; the body and paper textures are built entirely from CSS gradients and inline SVG noise, with no external images. The receipt content is just children, so you decide how items, tax and the order number are laid out.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add "https://webberui.com/r/receipt-printer.en.json?t=<install token>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ReceiptPrinter/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add "https://webberui.com/r/receipt-printer.en.json?t=<install token>"Or, once registries are configured in components.json, install it as @webberui/receipt-printer.
Usage
import * as React from "react";
import {
ReceiptPrinter,
type ReceiptStage,
} from "@/components/ui/receipt-printer";
export function CheckoutReceipt() {
const [stage, setStage] = React.useState<ReceiptStage>("idle");
async function pay() {
setStage("processing");
await chargeCard(); // your payment API
setStage("printing");
}
return (
<>
<button type="button" onClick={pay}>
Confirm payment
</button>
<ReceiptPrinter stage={stage} onStageChange={setStage}>
<p className="text-center font-bold">Timeworn Goods</p>
<dl className="mt-2 space-y-1 border-t border-dashed pt-2">
<div className="flex justify-between">
<dt>Annual plan</dt>
<dd>$96.00</dd>
</div>
<div className="flex justify-between font-semibold">
<dt>Total</dt>
<dd>$100.80</dd>
</div>
</dl>
</ReceiptPrinter>
</>
);
}If you don't need to wire it to a payment flow, use uncontrolled mode: after mounting it waits processingMs, prints on its own, and the ref's replay() runs it again.
const ref = React.useRef<ReceiptPrinterHandle>(null);
<ReceiptPrinter ref={ref} mode="smooth" duration={2.4} teeth={28}>
{/* receipt details */}
</ReceiptPrinter>;
ref.current?.replay();For a custom layout (for example, placing the status line elsewhere), use the compound parts:
<ReceiptPrinter.Root stage={stage} onStageChange={setStage}>
<ReceiptPrinter.Machine>
<ReceiptPrinter.Header>TP-58</ReceiptPrinter.Header>
<ReceiptPrinter.Screen />
</ReceiptPrinter.Machine>
<ReceiptPrinter.Output>
<ReceiptPrinter.Paper>{/* receipt details */}</ReceiptPrinter.Paper>
</ReceiptPrinter.Output>
<ReceiptPrinter.Status className="mt-3" />
</ReceiptPrinter.Root>Props
ReceiptPrinter and ReceiptPrinter.Root take the same props (children of ReceiptPrinter go inside the paper; children of Root are the parts you compose).
| Prop | Type | Default | Description |
|---|---|---|---|
stage | "idle" | "processing" | "printing" | "complete" | — | Controlled stage; managed internally when omitted |
defaultStage | ReceiptStage | "processing" | Initial stage in uncontrolled mode |
onStageChange | (stage: ReceiptStage) => void | — | Called on stage changes; the end of the paper feed reports "complete", and in controlled mode the parent decides whether to apply it |
mode | "stepped" | "smooth" | "stepped" | stepped feeds line by line in 20 jolts; smooth feeds in one ease-in-out motion |
duration | number | 1.75 | Total feed length in seconds; reads the CSS variable --wb-receipt-duration when omitted |
teeth | number | 40 | Number of teeth on the serrated bottom edge; tooth depth is fixed at 4px |
processingMs | number | 1200 | Milliseconds spent in processing when uncontrolled, before moving on to printing |
tearable | boolean | true | Whether the receipt can be torn off when complete (button, or dragging down more than 60px) |
onTear | () => void | — | Called after the tear-off animation ends |
labels | ReceiptPrinterLabels | — | Override interface and screen-reader copy |
grain | boolean | false | Overlay an extra grain layer on the body and paper |
ref | React.Ref<ReceiptPrinterHandle> | — | Exposes replay(): pulls the receipt back in and returns to processing |
className | string | — | Forwarded to the outermost container |
The Machine, Header, Output and Paper parts take children and className; Screen and Status take only className.
ReceiptPrinterLabels
| Field | Type | Default | Description |
|---|---|---|---|
device | string | "出單機" (Printer) | Device name on top of the body (shown when Header has no children) |
idle | string | "待命" (Ready) | Status text for the idle stage |
processing | string | "處理中…" (Processing…) | Status text for the processing stage |
printing | string | "列印收據…" (Printing receipt…) | Status text for the printing stage |
complete | string | "完成" (Done) | Status text for the complete stage |
srProcessing | string | "正在處理付款" (Processing payment) | Screen-reader announcement: processing |
srPrinting | string | "正在列印收據" (Printing receipt) | Screen-reader announcement: printing |
srComplete | string | "收據已完成" (Receipt complete) | Screen-reader announcement: complete |
srTorn | string | "收據已撕下" (Receipt torn off) | Screen-reader announcement: torn off |
tear | string | "撕下收據" (Tear off receipt) | Tear button text |
paper | string | "收據" (Receipt) | aria-label of the paper section |
The ReceiptStage, ReceiptFeedMode, ReceiptPrinterHandle, ReceiptPrinterProps and ReceiptPrinterRootProps types are also exported by name.
How it works
- Stepped feed:
steppedmode splits the 0→100% feed into 20 steps — 41 keyframe values alternating "advance, hold", with evenly spaced times and linear ease, so advancing and holding each take half the time. It reads as a thermal printer pushing paper out one notch at a time - Paper slot: the paper starts at
translateY(-100%), hidden inside the slot, and moves to 0 while printing. The slot usesclip-path: inset()to clip only the top edge, so the tilt, fall and shadow of the tear-off are not cut off - Controlled and uncontrolled: uncontrolled, it waits
processingMsin processing, prints, and completes on its own; controlled, the component only reportsonStageChange("complete")when the feed ends, and the parent decides when to move from processing to printing (usually once the payment API responds with success) - Rhythm variables:
--wb-receipt-duration(total feed length),--wb-receipt-accent(indicator and complete icon, default#34d399),--wb-receipt-processing(processing indicator, default#fbbf24),--wb-receipt-screen(screen text, default#bef264) - Tear-off: the tilt is picked at random between −8° and 8° at the moment of tearing, and the paper moves down 120% and fades out over 500ms ease-in; it resets when the stage returns to processing or
replay()is called - Dark mode: the paper stays a light paper color (
neutral-100) and the body is dark already, so it looks like a real receipt printer in both themes
Accessibility
- Root contains an
sr-onlyregion withrole="status"andaria-live="polite"that announces "Processing payment", "Printing receipt" and "Receipt complete" per stage, and "Receipt torn off" after a tear-off; the visual text in Screen and Status isaria-hiddento avoid double announcements - The paper is a
sectionwitharia-label="Receipt"; before printing and after tearing it isaria-hiddenandinert. Marking up the details withdlortableis recommended - "Tear off receipt" is a real
button: Tab-focusable, triggered with Enter or Space, with afocus-visiblering; drag-to-tear is only an extra pointer interaction - When the user has turned on reduce motion in their system settings, there is no paper feed and no jolts — entering printing shows the full receipt and the complete status right away; status text only fades without moving, the indicator does not pulse, and the tear-off becomes a fade
Coupon Code Input
An ecommerce coupon input React component: on success the field turns into a perforated ticket chip with a tear-off stub animation and a rolling discount amount; on failure it shakes and shows the reason. The applied chip is removable and input is case-normalised.
Transaction List
A finance-app transaction list React component: rows grouped by date automatically, rolling amounts coloured by sign, neighbouring rows shifting smoothly when a new transaction is inserted, plus swipe-to-pin and status chips.