Recursive AI

Headers, footers and page breaks

All onerender recipes

Control a PDF’s pages from CSS with OneRender: page size and margins with @page, a running header and “Page 1 of 5” footer in the margin boxes, forced page breaks, and a landscape page for one section.

C#
using System.Text;
using OneRender.Pdf;
 
var rows = new StringBuilder();
for (int i = 1; i <= 60; i++)
    rows.Append($"<tr><td>{i:000}</td><td>Order {1000 + i}</td><td>${i * 12.5m:0.00}</td></tr>");
 
string html = $$"""
<!DOCTYPE html>
<html lang="en">
<head>
<title>Quarterly report</title>
<style>
  @page {
    size: A4;
    margin: 25mm 20mm;
    @top-left { content: string(chapter); font: 9pt sans-serif; color: #666 }
    @bottom-right { content: "Page " counter(page) " of " counter(pages); font: 9pt sans-serif }
  }
  @page :first { @top-left { content: none } }
  @page wide { size: A4 landscape }
  body { font: 11pt/1.5 serif }
  h1 { string-set: chapter content() }
  .chapter { break-before: page }
  .wide { page: wide }
  table { width: 100%; border-collapse: collapse }
  th { background: #eee; text-align: left }
  td { padding: 4px 8px; border-bottom: 1px solid #ccc }
</style>
</head>
<body>
  <h1>Summary</h1>
  <p>The orders table starts on the next page.</p>
  <section class="chapter">
    <h1>Orders</h1>
    <table>
      <thead><tr><th>#</th><th>Order</th><th>Total</th></tr></thead>
      <tbody>{{rows}}</tbody>
    </table>
  </section>
  <section class="chapter wide">
    <h1>Appendix</h1>
    <p>This section is on a landscape page.</p>
  </section>
</body>
</html>
""";
 
HtmlPdfResult result = HtmlPdf.ToPdf(html, File.Create("report.pdf"));
Console.WriteLine($"{result.PageCount} pages");
Text
5 pages

Page 1 is the summary, with no header. The table starts on page 2 and continues on pages 3 and 4, with “Orders” in the top-left corner of each. Page 5 is the appendix, on a landscape A4 page. Every page has “Page n of 5” at the bottom right.

What each rule does

  • @page { size; margin } sets the page box. Sizes take keywords (A3, A4, A5, B4, B5, letter, legal, ledger), landscape or portrait, or one or two lengths. The page setup in HtmlPdfOptions is only the default for a document without @page.
  • Margin boxes (@top-left, @top-center, @bottom-right and the other 13) hold generated content: text, counter(page), counter(pages) and string(). They are drawn as artifacts, so the running header and page numbers stay out of the PDF’s structure tree and screen readers don’t read them on every page.
  • string-set copies an element’s text into a named string, and string(chapter) shows the current value on each page. :first, :left, :right and :blank select pages.
  • break-before: page (also break-after, and the older page-break-before) forces a new page. break-inside: avoid keeps a block together. orphans and widows apply to paragraphs.
  • Named pages. page: wide puts an element on pages of the @page wide rule, starting a new page when the name changes.

Tables across pages

A table breaks between rows, and a row is never split. In 1.0.0 a table that continues onto another page keeps room for its thead and tfoot at the top and bottom of that page, but the copies don’t show, so here page 3 starts with an empty band and then row 028. If each page of a long table needs its headings, split it into one table per page yourself.

Links to #id targets become links inside the PDF. A contents list can show page numbers with target-counter(attr(href), page) and dot leaders with leader(dotted):

CSS
.toc a::after { content: leader(dotted) target-counter(attr(href), page) }

Good to know

  • Footnotes, position: running() and vertical writing modes are not implemented.
  • Units in @page are CSS units; PdfPageSetup in the options takes CSS pixels (96 per inch).
  • A document that would need more than MaxPages pages throws PdfRenderException.

The paged-media rules are in Architecture and PDF output.