PDFreact-print-pdf
DocsBrowser-only alpha

Receipt (thermal 80mm)

A restaurant slip printed at the table. Narrow page, monospace stack, modifier lines, GST split, "PAID" stamp.

Edit this page

Live restaurant receipt preview\u00b7 Receipt 80mm

StackBlitzGitHub Workshop
Click Export PDF to download a real vector PDF. The on-screen pixels match the PDF output exactly.
// Restaurant thermal receipt — narrow (80mm) mono-styled, the kind printed// at the table when you ask for the bill. Picks $ rather than ₹ because the// standard PDF mono fallback (Courier) only encodes WinAnsi and would fall// back to "?" for the rupee sign; users who register a real mono font with// rupee glyphs can re-skin this trivially.import { Rand, makeRestaurantOrder, moneyFixed } from "./lib/data";const SEED = 0x1eaf;export function RestaurantReceiptFixture() {  const r = new Rand(SEED);  const order = makeRestaurantOrder(SEED, 7);  // Subtotal = sum of (qty × unit + modifier prices). Modifiers price is  // per-line, not per-quantity, matching how POS systems usually charge.  let subtotal = 0;  for (const it of order.items) {    subtotal += it.qty * it.unit;    for (const m of it.modifiers) subtotal += m.price;  }  const cgst = Math.round(subtotal * 0.025);  const sgst = Math.round(subtotal * 0.025);  const service = Math.round(subtotal * 0.05);  const total = subtotal + cgst + sgst + service;  const txn = `0c${r.int(0x1000, 0xffff).toString(16)}`.toUpperCase();  return (    // The whole receipt is `font-mono` so it falls back to Courier (or any    // mono the user registers) when exported. `text-[11px]` is the standard    // thermal-printer body size; centered runs use `text-center`. The    // ASCII separator rows are real text so they stay vector / selectable.    <div className="font-mono text-[11px] leading-[15px] text-gray-900">      {/* ── Header ───────────────────────────────────────────────────── */}      <header className="text-center">        <p className="text-[14px] font-bold uppercase tracking-[0.18em]">Little Fern</p>        <p className="mt-0.5 text-[10px] text-gray-700">Bistro &middot; Bar &middot; Cafe</p>        <p className="mt-2 text-[10px] leading-[14px] text-gray-700">          221 Plum Court, Bengaluru 560001          <br />          +91 80 4123 5678          <br />          GSTIN 29ABCDE1234F2Z5        </p>      </header>      <Sep />      {/* ── Order meta ──────────────────────────────────────────────── */}      <dl className="grid grid-cols-2 gap-x-3 gap-y-0.5 text-[10px]">        <dt className="text-gray-600">Order</dt>        <dd className="text-right">#{order.orderNo}</dd>        <dt className="text-gray-600">Date</dt>        <dd className="text-right">12 Apr 2026 19:42</dd>        <dt className="text-gray-600">Table</dt>        <dd className="text-right">{order.table}</dd>        <dt className="text-gray-600">Server</dt>        <dd className="text-right">{order.server}</dd>        <dt className="text-gray-600">Guests</dt>        <dd className="text-right">{order.guests}</dd>      </dl>      <Sep />      {/* ── Items ───────────────────────────────────────────────────── */}      <div className="text-[10px] uppercase tracking-[0.14em] text-gray-600">        <div className="flex items-baseline">          <span className="w-6 shrink-0">Qty</span>          <span className="flex-1">Item</span>          <span className="w-16 shrink-0 text-right">Amount</span>        </div>      </div>      <Sep dashed />      <ul className="space-y-1">        {order.items.map((it) => {          const lineBase = it.qty * it.unit;          return (            <li key={`${it.name}`}>              <div className="flex items-baseline">                <span className="w-6 shrink-0 tabular-nums">{it.qty}×</span>                <span className="flex-1 pr-2">{it.name}</span>                <span className="w-16 shrink-0 text-right tabular-nums">                  {moneyFixed(lineBase)}                </span>              </div>              {it.modifiers.map((m) => (                <div key={m.name} className="flex items-baseline pl-6 text-gray-600">                  <span className="flex-1 pr-2">+ {m.name}</span>                  <span className="w-16 shrink-0 text-right tabular-nums">                    {moneyFixed(m.price)}                  </span>                </div>              ))}            </li>          );        })}      </ul>      <Sep />      {/* ── Totals ──────────────────────────────────────────────────── */}      <dl className="space-y-0.5">        <Row label="Subtotal" value={moneyFixed(subtotal)} />        <Row label="CGST 2.5%" value={moneyFixed(cgst)} muted />        <Row label="SGST 2.5%" value={moneyFixed(sgst)} muted />        <Row label="Service 5%" value={moneyFixed(service)} muted />      </dl>      <Sep dashed />      <div className="flex items-baseline">        <span className="flex-1 text-[12px] font-bold uppercase tracking-[0.12em]">Total</span>        <span className="w-20 shrink-0 text-right text-[14px] font-bold tabular-nums">          ${moneyFixed(total)}        </span>      </div>      <Sep />      {/* ── Payment ─────────────────────────────────────────────────── */}      <div className="text-center text-[10px] leading-[14px]">        <p>          <span className="border border-gray-900 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-[0.14em]">            Paid          </span>          <span className="ml-2 text-gray-700">UPI · *@oksbi</span>        </p>        <p className="mt-1 text-gray-600">TXN {txn}</p>      </div>      <Sep />      {/* ── Footer ──────────────────────────────────────────────────── */}      <footer className="text-center text-[10px] leading-[14px] text-gray-700">        <p className="font-bold text-gray-900">Thank you · come again</p>        <p className="mt-1">www.littlefern.example</p>        <p className="mt-2 text-[9px] tracking-[0.16em] text-gray-500">|||| | || |||| | || ||||</p>      </footer>    </div>  );}/** Full-width row: left label, right value, both monospaced. */function Row({ label, value, muted }: { label: string; value: string; muted?: boolean }) {  return (    <div className={`flex items-baseline ${muted ? "text-gray-700" : ""}`}>      <span className="flex-1">{label}</span>      <span className="w-20 shrink-0 text-right tabular-nums">{value}</span>    </div>  );}/** Horizontal rule drawn as a 1px border. We use a real CSS border (not an * ASCII row of dashes) so the line stays sharp at any export resolution and * doesn't depend on the chosen mono font having a `─` glyph. */function Sep({ dashed = false }: { dashed?: boolean }) {  return (    <div      className={`my-2 border-t ${dashed ? "border-dashed border-gray-400" : "border-gray-900"}`}    />  );}
Live <PrintPreview> · Receipt 80mmOpen in new tab
Pages are rendered inline as scaled DOM — no PDF round-trip. The same paginator drives exportToPDF, so the page breaks you see here are the page breaks you'll get in the file.

What you're looking at

A complete thermal-printer receipt — the kind a Bistro / Bar / Cafe POS prints after you ask for the bill. It exercises the patterns that come up every time a team wants their "screen receipt" to also be a downloadable PDF:

  • Custom paper size. 80mm wide × 240mm tall. The library takes a [width_mm, height_mm] tuple, so anything off the A-series chart works the same way.
  • Monospaced everything. Body text is a real mono font (falls back to Courier). The library subsets and embeds the font into the PDF, so the receipt looks identical on every machine that opens it.
  • Modifier lines. Indented sub-rows under each item ("+ extra cheese", "+ no onion"). Vector text means the modifier names stay searchable in the PDF.
  • GST split + service charge. Three muted rows under the subtotal, one bold total. No special PDF logic — it's the same Tailwind utilities you'd use for the screen.
  • "PAID" stamp. A bordered inline-block with letter-spacing — vector, not raster.
  • ASCII barcode strip in the footer. Real text characters, so the user can copy them. (Swap for an <img src="...png"> if you want a scannable code.)

The exporter call

Two pieces. The first is the size preset:

const RECEIPT_80MM = {
  format: [80, 240],   // mm × mm, the library converts internally to points
  margin: "4mm",       // small margins on a thermal slip
  kind: "thermal",
};

The second is the actual export, using the React DX layer:

import { usePDFExport, Printable, ExportButton } from "react-print-pdf/react";

function ReceiptPage({ order }) {
  const exporter = usePDFExport({
    fileName: `receipt-${order.orderNo}.pdf`,
    paperSize: RECEIPT_80MM.format,
    margins: RECEIPT_80MM.margin,
  });

  return (
    <>
      <ExportButton target={exporter.ref}>Download receipt</ExportButton>
      <Printable ref={exporter.ref} options={exporter.options}>
        <RestaurantReceipt order={order} />
      </Printable>
    </>
  );
}

That's all the export plumbing. Everything else is the component itself.

The component

Full source on GitHub — 164 lines, plain React + Tailwind.

The structural skeleton:

export function RestaurantReceipt({ order }) {
  // 1. Compute totals from items + modifiers.
  let subtotal = 0;
  for (const it of order.items) {
    subtotal += it.qty * it.unit;
    for (const m of it.modifiers) subtotal += m.price;
  }
  const cgst    = Math.round(subtotal * 0.025);
  const sgst    = Math.round(subtotal * 0.025);
  const service = Math.round(subtotal * 0.05);
  const total   = subtotal + cgst + sgst + service;

  return (
    <div className="font-mono text-[11px] leading-[15px] text-gray-900">
      <Header />               {/* business name, address, GSTIN */}
      <Sep />
      <OrderMeta order={order} />
      <Sep />
      <ItemColumns />
      <Sep dashed />
      <ItemList items={order.items} />
      <Sep />
      <TotalsRows {...{ subtotal, cgst, sgst, service }} />
      <Sep dashed />
      <GrandTotal total={total} />
      <Sep />
      <PaymentBlock txn={order.txn} />
      <Sep />
      <ReceiptFooter />
    </div>
  );
}

/** Horizontal rule drawn as a real 1px border. We don't print a row of
 * ASCII '-' or '─' characters because the chosen mono fallback (Courier)
 * doesn't always have the box-drawing glyph, and a CSS border stays sharp
 * at any export resolution. */
function Sep({ dashed = false }) {
  return (
    <div
      className={`my-2 border-t ${dashed ? "border-dashed border-gray-400" : "border-gray-900"}`}
    />
  );
}

Why the choices look the way they do

A couple of decisions in this fixture are deliberate, not stylistic. Worth calling out:

Why border-t for the separator and not '-'.repeat(32)

Because the separator is part of the layout, not part of the text. Drawing it as a CSS border keeps it sharp at any export resolution and survives a future change of mono font without checking whether that font ships a glyph. WinAnsi encoding — what pdf-lib's built-in fonts use — doesn't include box-drawing characters; tracking which custom fonts do gets tedious fast.

Why $ and not

The fixture uses $ as the currency symbol even though it's a Bengaluru bistro. Reason: the standard PDF built-in mono font (Courier) only encodes the WinAnsi range, and the rupee sign isn't in there. If you register a real mono TTF with rupee glyphs (and call registerFont before export), you can swap freely:

await registerFont({
  family: "Mono",
  url: "/fonts/IBMPlexMono-Regular.woff2",
  weight: 400,
});

Why the totals are integers

POS systems usually quote whole rupees / cents / paise — they round at line-item time, not at the display step. The fixture follows that convention so Math.round shows up explicitly rather than getting hidden behind a toFixed(2). Swap to fractional cents trivially with a Money helper.

Customizing it

The smallest "swap your branding in" path:

  1. Replace the Header block with your own business name + address.
  2. Edit RECEIPT_80MM if you're on 58mm thermal paper instead.
  3. Provide your own order shape — { orderNo, table, server, guests, items: [{ name, qty, unit, modifiers: [{ name, price }] }] }.
  4. Register a mono font with the glyphs your locale needs (currency symbols, accented characters).

Total work: ~20 minutes to swap a real menu in.

  • Best practices — width-matching, font registration, separators
  • Fonts — registering a custom mono font with currency glyphs
  • Sizing and alignment — how the custom paper size flows to pageContentWidth

On this page