busy-office-ui
Menu

Pattern: issued document

Who uses it: nobody, on screen — this is the artefact the ERP emits: a purchase order sent to a vendor, an invoice, a delivery note travelling with goods. How often: every time a document is issued, usually unattended. What "done" looks like: a page a stranger can read without your system — sender, recipient, document number, lines, totals, terms — that survives pagination and a black-and-white printer. Not a screen, and not a PDF engine — if a person needs to work with the record, that is the object page; and rendering the actual PDF is your server's job (see the grill verdict below), not this framework's.

Does this belong in a CSS framework at all? — the grill verdict

The Accept for this pattern asked the question outright, and the honest answer is half yes, so it is written down rather than assumed:

  • Not ours: producing the PDF. Headless Chrome, Gotenberg, wkhtmltopdf, a print server — that is infrastructure, it belongs to your stack, and a CSS framework that shipped one would be the virtualiser mistake again.
  • Ours: what every one of those tools consumes — HTML and a print stylesheet. The document's shape, the page contract (@page margins, repeating table headers and totals, break control) and the black-and-white legibility rules are exactly this framework's job, and they already ship in the print layer.
  • So this page documents the contract, not a generator: give your renderer this markup and these classes, and the output is a document. Zero new CSS was needed to build it — which is itself the evidence the contract was already complete.

The document

Busy Office Supplies Ltd

14 Harbour Road · Rotterdam · NL
VAT NL0012 3456 B01

Purchase order

PO-88213
Issued 22 Aug 2026

Vendor

Acme Supply Co.
221 Industrial Way
Manchester · UK

Deliver to

Dock 4, 123 Warehouse Rd
Rotterdam · NL

Payment terms
Net 30
Incoterms
DAP Rotterdam
Requested delivery
15 Sep 2026
Buyer
J. Kim
Order lines for PO-88213
Material Description Qty Unit price Net value
MAT-4471Hydraulic pump, 24V12770.009,240.00
MAT-1180Seal kit, 60 mm4037.011,480.50
MAT-9002Filter cartridge15054.808,220.00
Net total18,940.50
VAT 21%3,977.51
Total EUR22,918.01

Terms: goods remain the property of the seller until paid in full. Quote PO-88213 on all correspondence and invoices.

Page totals continue overleaf · PO-88213

On paper this is one document, not a screenshot of a screen: the table header repeats on every page and so do the totals (<tfoot> is table-footer-group by browser default — measured, not assumed, which is why the framework adds no rule for it), the address blocks never split (.bo-u-avoid-break), and the print-only line appears only on paper.

Logo & barcode — any mark, any symbology

Both are supported the same way, and the split is the same as the PDF one: the framework does not generate either — barcode bitmaps come from a library (bwip-js, ZXing, a GS1 service) or a barcode font, and a logo is your asset. What decides whether the printed result actually works is placement, and that is entirely CSS:

RuleWhy it decides success
.bo-u-print-exact on the markThe one that breaks scanning. Browsers lighten fills and drop backgrounds when printing; a barcode whose bars come back grey does not scan, and a logo loses its mark. This utility says "these colours are content".
Quiet zone — blank margin around the symbolEvery symbology requires one (roughly 10× the narrow-bar width for Code 128, 4 modules for QR). A barcode flush against a table border is unreadable to the scanner even though it looks fine.
Physical units (mm), not pxA barcode has a real-world minimum size. px is a screen unit and its printed size depends on the renderer's DPI assumption; mm means the same thing on every printer.
SVG for the symbol where possibleVector bars stay crisp at any DPI. For a raster barcode add image-rendering: pixelated so the renderer does not smooth the bar edges into each other.
.bo-u-avoid-break on the blockA symbol split across a page boundary is two unreadable halves.
Human-readable text under the symbolBoth the two-channel rule and the actual GS1 standard: the number must be printed as text as well, so a human (or a failed scan) can still read it.
PO-88213
BO
Logo · 26mm
<!-- Barcode: your renderer supplies the image; these classes and
     units are what make the printed result scan. -->
<figure class="bo-u-print-exact bo-u-avoid-break">
  <img src="/barcode/PO-88213.svg" alt="Barcode for document PO-88213"
       style="inline-size: 48mm; block-size: 14mm; padding: 3mm" />
  <figcaption>PO-88213</figcaption>   <!-- human-readable, always -->
</figure>

<!-- Logo: same utility, physical size, real alt text. -->
<img class="bo-u-print-exact" src="/brand/logo.svg"
     alt="Busy Office Supplies Ltd" style="inline-size: 26mm" />

This closes a loop the framework already had open on the other side: initScanInput() on the RF goods-receipt screen reads these barcodes off a handheld. The document that gets scanned in the warehouse is the one this pattern emits — emit and read are now both documented.

Page numbers & running headers — what browsers do, and what needs a renderer

Compared against the dedicated paged-media engines (Prince, WeasyPrint), which is what the CSS Paged Media and GCPM specs were written for. Two of their headline features matter for an ERP document, and they do not land the same way — measured here by printing a three-page table to PDF and reading the text back out, not by consulting a support table:

FeatureBrowser print / headless ChromePrince · WeasyPrint
@page margin box with counter(page) / counter(pages) Works — rendered "1 of 3", "2 of 3", "3 of 3" on the right pages Works
string-set + content: string(…) running headers No — emitted nothing at all in the same test Works (GCPM)
Repeating <thead> / <tfoot> Works — header and totals repeated on all 3 pages Works

So page numbering is available to you today, in a plain browser, with no renderer and no JavaScript. It is not shipped as a framework default because @page is a document-global rule — it cannot be scoped to one route or one element, so shipping it would stamp page numbers on every print job in every consuming app, including screens where that is wrong. It belongs in the stylesheet of your document route:

/* your-document-route.css — paste as-is; verified in Chrome */
@page {
  margin: 1.6cm;                       /* the framework already sets this */
  @bottom-center {
    content: "Page " counter(page) " of " counter(pages);
    font-size: 9pt;
  }
  @bottom-right { content: "PO-88213"; }   /* the doc number on every page */
}

/* Server-renderer ONLY (Prince, WeasyPrint) — a running header that
   repeats the document title. Browsers ignore string-set entirely, so
   never rely on it for a document a user might print from the app. */
h1 { string-set: doctitle content(); }
@page { @top-center { content: string(doctitle); } }

The document above uses a plain .bo-u-print-only line carrying its number instead, because that works in both paths — the browser print dialog and the server renderer — and a loose page from a printed PO still identifies itself. Add the margin-box recipe on top when the document is rendered by a route you control.

Markup

<!-- The whole document is one .bo-print-report. No new classes:
     letterhead + parties + kv meta + lines + tfoot totals + terms. -->
<article class="bo-print-report">
  <header class="bo-u-avoid-break"> …sender / doc number / date… </header>

  <div class="bo-form-row bo-u-avoid-break"> …vendor / deliver-to… </div>

  <dl class="bo-kv bo-u-avoid-break"> …terms, incoterms, buyer… </dl>

  <div class="bo-data-table-container" tabindex="0">  <!-- screen: narrow
       viewports scroll the lines like a PDF; print ignores it -->
  <table class="bo-data-table" data-density="compact">
    <thead>…</thead>            <!-- repeats on every printed page -->
    <tbody>…lines…</tbody>
    <tfoot>…net / VAT / total…</tfoot>  <!-- repeats too, by default -->
  </table>
  </div>

  <footer class="bo-u-avoid-break">
    <p>Terms…</p>
    <p class="bo-u-print-only">Page totals continue overleaf · PO-88213</p>
  </footer>
</article>

Anatomy

  1. Letterhead — who is sending, with the legal identifiers (VAT/company number) a document needs to be valid.
  2. Document identity — type, number, issue date. The number is the thing every later conversation quotes. Not itself a monitoring signal — the human-monitoring surface for WHETHER documents were emitted lives on report and job monitor; this region is what makes an artefact self-evidencing once someone does open it.
  3. Parties — vendor and ship-to as separate blocks, each .bo-u-avoid-break: an address split across a page boundary is a returned delivery.
  4. Meta — key-value facts for terms, incoterms, dates, buyer.
  5. Lines + totals — a data table with a real <tfoot>; both header and totals repeat per page because the print layer and the browser default already say so.
  6. Terms & footer — the legal tail, plus .bo-u-print-only content that exists on paper only.

Data contract

RequestReturns / on failure
GET /po/:id/documentthis HTML, standalone (no app shell, no nav) — a complete page your renderer or a browser can print directly. 404 if the record does not exist; 5xx if rendering itself fails — both as a plain error page, never a broken document that LOOKS complete
GET /po/:id/document.pdfyour server runs the SAME HTML through its renderer; the PDF and the printed page cannot disagree because they are one source
issue / re-issuea re-issued document states it ("Copy · re-issued 24 Aug 2026") — an unmarked duplicate of a financial document is a control failure
drafta document that is not yet issued must SAY so on its face (see states) — never render a draft that looks issued

The AI-manages / human-monitors split, stated: agents generate and dispatch these unattended, so the document itself carries every check a human later needs — the number, the issue date, the draft/copy marking, the totals. The monitoring surface for whether they were emitted is the report and the job monitor; this page's job is that each emitted artefact is self-evidencing.

States

StateWhat ships
Draftthe word DRAFT in the document identity block and in a print-only line — never a colour wash or a watermark image, which a black-and-white printer or a forced-colors reader loses entirely
Issuedissue date present; the document is the record of what was sent
Copy / re-issuemarked in text with the re-issue date beside the original
Cancelledstated in words at the top; the lines stay readable because a cancelled document still has to be auditable
Multi-pageheader and totals repeat; parties and terms blocks never split; the print-only continuation line carries the document number so a loose page is identifiable
No linesrefuse to emit — a document with no lines is a bug upstream, not an empty state to style
No JSfully functional — this page has no JS dependency; it's a rendered, printable document (the initScanInput() mentioned here belongs to the separate goods-receipt screen that later reads its barcode, not to this page)

Components used

complexity 2 of 4

↑↓ move Enter open Esc close