Skip to content
CastPDF

An HTML invoice template built for print

A good HTML invoice template sets its paper size with @page and keeps column names in a <thead>. It holds the totals in one block that never splits, uses fonts the renderer has, and loads its logo from a public URL. Copy the template below, or start from ours, and render it with our invoice PDF API.

  • Updated October 2026
  • 100 free PDFs a month

Why a web invoice is not a print invoice

An invoice page that looks perfect in a browser tab can fall apart as a PDF. The browser window has no fixed height, so a long table just scrolls. Paper has edges. Rows have to stop at the bottom of one sheet and carry on at the top of the next, and the reader still needs to know which column is which.

Print also brings small traps. A web font that loads late leaves a fallback in the PDF. A logo on your laptop is invisible to a server. A pale grey that looks elegant on a monitor fades to nothing on an office laser printer. A template built for print plans for all of these from the first line.

What makes a good print template

The parts of a print-ready invoice
PartWhy it mattersWhat to write
Paper size and marginsEvery page has the same frame, with room for a page number.@page with size and margin
Header rowColumn names come back on page two and beyond.A <thead> with <th> cells
Whole rowsNo line item is cut in half at the page edge.break-inside: avoid on tr
Totals blockThe total never sits alone at the top of a new page.One wrapper with break-inside: avoid
FontsThe PDF uses the type you designed with.An installed family first, then fallbacks
LogoYour brand shows on every copy.An <img> with a public https or data: source
ColourText stays readable on any printer.Dark text, colour for accents only

The template to copy

Here is a complete invoice in under 100 lines. It is a Liquid template, so the values in double braces come from your JSON. Paste it into the dashboard editor with some sample data, or keep it in your repository and send it as html.

invoice.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
  @page {
    size: A4;
    margin: 18mm 16mm 22mm;
    @bottom-right { content: "Page " counter(page) " of " counter(pages); font: 8pt "Noto Sans", sans-serif; color: #555; }
  }
  body { margin: 0; font: 10pt/1.45 "Noto Sans", "Liberation Sans", sans-serif; color: #1a1a1a; }
  .top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10mm; }
  .logo { height: 14mm; width: auto; }
  h1 { margin: 0; font-size: 22pt; font-weight: 600; color: #8a2b06; text-align: right; }
  .meta { margin: 2mm 0 0; text-align: right; }
  .parties { display: flex; gap: 12mm; margin-bottom: 8mm; }
  .parties div { flex: 1; }
  .label { margin: 0 0 1mm; font-size: 7.5pt; letter-spacing: 0.08em; text-transform: uppercase; color: #8a2b06; }
  table.lines { width: 100%; border-collapse: collapse; }
  thead { display: table-header-group; }
  th { padding: 2mm; background: #f6ece6; text-align: left; font-size: 8.5pt; }
  td { padding: 2mm; border-bottom: 0.3mm solid #ddd; vertical-align: top; }
  tr { break-inside: avoid; }
  .num { text-align: right; white-space: nowrap; font-variant-numeric: tabular-nums; }
  .summary { break-inside: avoid; margin-top: 6mm; margin-left: auto; width: 70mm; }
  .summary p { display: flex; justify-content: space-between; margin: 0; padding: 1.2mm 0; }
  .summary .due { border-top: 0.5mm solid #1a1a1a; font-weight: 700; font-size: 12pt; }
  .footer { margin-top: 10mm; font-size: 8.5pt; color: #444; }
</style>
</head>
<body>
  <div class="top">
    <img class="logo" src="{{ seller.logo_url }}" alt="{{ seller.name }}">
    <div>
      <h1>Invoice</h1>
      <p class="meta">No. {{ number }}<br>Issued {{ issued | format_date: "long" }}<br>Due {{ due | format_date: "long" }}</p>
    </div>
  </div>

  <div class="parties">
    <div>
      <p class="label">From</p>
      <p>{{ seller.name }}<br>{{ seller.address }}<br>{{ seller.tax_id }}</p>
    </div>
    <div>
      <p class="label">Bill to</p>
      <p>{{ buyer.name }}<br>{{ buyer.address }}</p>
    </div>
  </div>

  {% assign subtotal = 0 %}
  <table class="lines">
    <thead>
      <tr><th>Item</th><th class="num">Qty</th><th class="num">Price</th><th class="num">Amount</th></tr>
    </thead>
    <tbody>
      {% for line in lines %}
      {% assign amount = line.qty | times: line.price %}
      {% assign subtotal = subtotal | plus: amount %}
      <tr>
        <td>{{ line.name }}</td>
        <td class="num">{{ line.qty }}</td>
        <td class="num">{{ line.price | money: currency }}</td>
        <td class="num">{{ amount | money: currency }}</td>
      </tr>
      {% endfor %}
    </tbody>
  </table>

  {% assign vat = subtotal | times: vat_rate | divided_by: 100 %}
  <div class="summary">
    <p><span>Subtotal</span><span class="num">{{ subtotal | money: currency }}</span></p>
    <p><span>VAT {{ vat_rate }}%</span><span class="num">{{ vat | money: currency }}</span></p>
    <p class="due"><span>Amount due</span><span class="num">{{ subtotal | plus: vat | money: currency }}</span></p>
  </div>

  <div class="footer">
    <p>{{ payment_note }}</p>
  </div>
</body>
</html>

And a small data set to try it with. The logo here is a placeholder: point it at a real image on a public address.

invoice-sample.json
{
  "number": "2026-117",
  "issued": "2026-10-03",
  "due": "2026-11-02",
  "currency": "GBP",
  "vat_rate": 20,
  "seller": { "name": "Kiln & Copper Ltd", "address": "4 Brewery Yard, Bristol BS1 6QH", "tax_id": "VAT GB 123 4567 89", "logo_url": "https://cdn.example.com/kiln-copper-logo.png" },
  "buyer": { "name": "Harbour Lane Cafe", "address": "22 Harbour Lane, Falmouth TR11 3DF" },
  "lines": [
    { "name": "Espresso cups, set of 6", "qty": 4, "price": 42 },
    { "name": "Hand-thrown serving bowl", "qty": 2, "price": 65 },
    { "name": "Delivery and packing", "qty": 1, "price": 18 }
  ],
  "payment_note": "Please pay by bank transfer within 30 days, quoting the invoice number."
}

The page: size, margins and numbers

The @page rule at the top of the style sheet sets A4 paper and the margins around the content. The bottom margin is a little deeper than the top because it holds the page counter. In the print CSS guide, the margin boxes such as @bottom-right are explained in full. They work in print mode, which saved templates use by default.

One thing to know: a size written in your CSS wins over the paper size in the template settings or the request. Customers in North America usually want Letter. If you serve both markets, delete the size line and set the format per request instead, while keeping the margin box for the page number.

The line items: a real table with a real header

The items sit in a plain <table>, never a stack of divs. The column names live in <thead>, and the CSS restates display: table-header-group so no framework style can quietly turn it off. In print mode the header row then returns at the top of every page the table reaches.

Each row carries break-inside: avoid, so an item with a long description moves to the next page whole. Amounts are right-aligned with tabular-nums, which gives every digit the same width. Columns of money then line up at the decimal point, which makes them far quicker to scan. The repeating header guide covers the rare cases where a header still goes missing.

The totals block: kept in one piece

Subtotal, VAT and amount due belong together. If a page break fell between them, the reader would find the final figure on a page of its own, detached from what it adds up. The .summary wrapper prevents that with break-inside: avoid. When the block does not fit below the last row, all three lines move to the next page as a group.

CastPDF also treats any element with the class keep-together the same way, so <div class="summary keep-together"> would work too. Use the same idea for payment details or a signature line.

Fonts the renderer already has

The template asks for Noto Sans, then Liberation Sans. Both are installed on the renderer, along with DejaVu, so the PDF uses them without a single download. Noto also covers accented letters, Greek, Cyrillic, Arabic and the CJK scripts, which matters the day a customer name arrives in a script you did not test.

Want your brand typeface instead? Load it as you would on a website, from a public https address or embedded as a data: URL. The renderer waits for fonts before it prints. Always keep an installed family at the end of the list as a safety net.

The logo: public, sized, sharp

The renderer fetches images itself, from the public internet. A logo on localhost, on an office network or behind a login cannot be reached, and the PDF will simply show a gap. For a small logo, a data: URL avoids the network entirely. An SVG stays crisp at any zoom; a PNG should be exported at two or three times the size it appears on screen.

Size the image in millimetres, as the .logo rule does with height: 14mm. Pixel sizes on paper depend on assumptions about screen density, while millimetres mean the same thing on every page.

Colours that survive the printer

Backgrounds print exactly as you style them: CastPDF turns on background printing for you, so the tinted header row appears in the PDF. Whether it looks good on paper is another question. Many invoices end up on a black and white office printer.

  • Keep body text near black. Light grey text turns faint or patchy on many printers.
  • Use your brand colour for headings, labels and thin rules, not for long passages.
  • Make tints light, so black text on top stays sharp in greyscale.
  • Never let colour alone carry meaning. Write "Overdue" in words as well as in red.

Render it and check the result

To test from a terminal, build the request with jq and send it with curl. Add a few dozen extra lines to the sample data to see the header repeat and the totals move as one block. A free test key works for this; its PDFs carry a watermark.

Render invoice.html with the sample data
jq -n --rawfile html invoice.html --slurpfile data invoice-sample.json \
  '{html: $html, data: $data[0], mode: "print"}' |
curl https://api.castpdf.com/v1/pdf \
  -H "Authorization: Bearer $CASTPDF_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @- \
  --output invoice.pdf

When the layout is right, save it as a template so your app sends only data. The invoice from JSON guide shows that request, and the API reference lists every option. Prefer a head start? Our invoice template already applies every rule on this page. To fill it from card payments, read custom invoices from payment events.

FAQ

Common questions

Should an HTML invoice use tables or divs for the line items?

Use a real table. Only table rows inside a thead can repeat on each page, and table cells keep columns aligned without extra CSS. Use divs and flexbox for the parts around the table.

What page margins suit a printed invoice?

Somewhere between 15 and 20 millimetres works well on A4 and Letter. Leave a little more at the bottom if you print page numbers there.

Why does my logo not appear in the PDF?

The server that renders the PDF cannot see files on your computer or private network. Host the logo at a public https address, or embed it as a data URL.

Which font should an invoice template use?

A clear sans serif with good number shapes. Noto Sans, Liberation Sans and DejaVu Sans are installed on the renderer, so they always work without loading anything.

Will background colours print in the PDF?

Yes, backgrounds and colours are printed as styled. Keep tints light and text dark, because many invoices are printed in greyscale later.

Should I put the page size in my CSS or in the request?

Put it in the request or template settings if you serve both A4 and Letter customers. A size in your CSS always wins, which suits a template that must never change paper.

Make your first PDF in 5 minutes

Pick a template, add your details and download your PDF. You get 100 free PDFs every month, and you don’t need a card.