Build a print-friendly receipt and report
By the end of this recipe you'll have a record view that prints — or exports to PDF via the browser's print dialog — at the same quality as the on-screen experience, with controlled page breaks, a header on every page, a monochrome palette, and zero leftover chrome.
Overview
A printable view is a second view of the same record. Same data, different layout, same care.
The recipe takes a record (a sale, an invoice, a report) and gives you a second rendering that the browser's print pipeline can turn into paper or PDF. It uses a @media print stylesheet to drop the nav and sidebar, swap to a serif body, force a monochrome palette, and control where the page breaks. A "Print" button calls window.print() — no PDF library required.
Never use the browser default. Always design the print version with the same care as the screen version. Customers see the receipt; treat it as marketing.
Skip this recipe if your receipts and reports already live in a server-rendered PDF pipeline. Browser-printed PDFs are perfect for SMB volumes; if you're issuing 50k tax invoices a night, use a real PDF renderer.
What you'll build
Three print artefacts from the same React tree, each tuned to its medium.
MUKAMBA GROUP
SALES REPORT
INVOICE
Required pieces
A small surface — most of the work is the @media print stylesheet, not new components.
@corelithzw/react exports used here: Button, Card, Stack. Plus a regular CSS file with @media print rules.
Step-by-step build
Mark which DOM is printable and which isn't
The screen view contains chrome — sidebar, toolbar, action menus — that shouldn't print. Tag every printable region with data-print="region" and everything else with data-print="hide". That single attribute is what the print stylesheet will key off; no className wrangling.
data-print="region" → CSS gives full page width
No JS branch for print — same React tree, two stylesheets.
Switch to the Code tab to see the snippet.
Write the print stylesheet
Reset margins on @page, drop the nav, force monochrome, and choose a paper-friendly type. The order matters: @page first, then root resets, then component-specific overrides. Keep the file under 80 lines — anything more and you're redesigning, not adapting.
Add a running header and footer for multi-page reports
For a one-page receipt you're done; reports need a brand mark and page number on every page. position: fixed on a sub-tree inside @media print behaves as a running header. Combine with page-break-before: always on section titles to control where pages break.
Gross sales for May rose 4.2% against April, driven by the Bulawayo outlet's weekend trading days. The pages that follow break out branches, categories, and refunds.
Trigger print from a button and reset the trigger
The print button calls window.print() directly. For the thermal-receipt mode you add a class to <html> before printing, then remove it in the afterprint event. Don't try to detect "did the user actually print?" — you can't.
Final composition
The whole ReceiptView + print.css. Drop both into your app; the rest of your shell stays as-is.
| Item | Amount |
|---|---|
| 2× Coke 500ml | $3.00 |
| 1× Bread | $1.20 |
| 3× Eggs | $2.10 |
Thank you. Return within 7 days with this receipt.
Variations
Three forks. Each is a small diff from the final composition.
PDF export via window.print()
The "Save as PDF" option in every browser's print dialog is already a PDF exporter. Label the button accordingly; the file the user gets is genuinely a PDF.
// Just rename the button
<Button onClick={print} variant="primary">
Save as PDF
</Button>
{/* The browser dialog already
offers PDF as a destination. */}
Printable invoice with VAT
An invoice differs from a receipt only by metadata and signature block. Reuse the same view, hide payment lines, and add a "Bill to" panel.
// Reuse ReceiptView, swap header
<header className="invoice-head">
<h1>INVOICE</h1>
<BillTo party={sale.buyer} />
</header>
// add to print.css
.invoice-head h1 { font: 700 22pt/1 monospace; }
Batch print of records
Print N receipts back-to-back. Each receipt gets a hard page break before; the print dialog still opens once.
// BatchPrint.tsx
{sales.map((s) => (
<section className="report-page" key={s.id}>
<ReceiptView sale={s} />
</section>
))}
// print.css already breaks per .report-page
Accessibility
Print accessibility is the same problem as screen accessibility — semantic markup that survives whatever rendering pipeline the browser picks.
- The receipt is a real
<article>with a real<table>. Screen readers and PDF accessibility checkers can both read the structure. No "div soup" line items. - Numeric columns use
<th scope="col">. The totals row uses<th scope="row">Total</th>so the row label is announced with the amount. - Print button is a real
<button>. Keyboard activates it; it lives in the document order, not behind a hover menu. - Link URLs print.
a[href]::after { content: " (" attr(href) ")" }means a printed copy is still useful when the link can't be clicked. - Colour is never load-bearing. The monochrome stylesheet means anyone printing on a B/W laser still sees the same hierarchy.
- Focus is preserved across
afterprint. The button that triggered the dialog remains focused — Tab order picks up where it left off.