Takumi

Headers & footers

Repeat headers, footers, and page numbers on every PDF page, and size the margins they need.

Pass JSX, an HTML string, or a node tree to header or footer. The band repeats in the top or bottom margin of every page, and automatic margins grow to fit it.

Repeat a page footer
import {  } from "takumi-pdf";
import { ,  } from "takumi-pdf/primitives";

const  = await (, {
  : (
    < ="flex w-full justify-center text-[10px] text-gray-500">
      Page < /> of < />
    </>
  ),
});

Pages that differ

pages overrides the header or footer on some pages. Each page looks up a band in this order:

  1. first or last
  2. odd or even
  3. the top-level header or footer

false leaves that band empty. Pages count from 1 over the whole document, before pageRanges drops any.

No header on the cover, a closing note on the last page
import {  } from "takumi-pdf";
import {  } from "takumi-pdf/primitives";

const  = await (, {
  : <>Acme Ltd</>,
  : < />,
  : {
    : { : false },
    : {
      : (
        <>
          <>Thank you for your business.</>
          <>Terms: net 30.</>
        </>
      ),
    },
  },
});

Automatic margins fit the tallest band on every page, so a taller last-page footer does not change where the other pages break. A one-page document is its own first and last page, and first wins.

Sizing the margin

margin defaults to "auto". The renderer measures the header and footer at full page width. Each automatic top or bottom margin is the band height plus a 20px inset, and at least 37.8px. Left and right margins are 37.8px. A numeric margin overrides that side, and sides left out of the object stay automatic:

Override the top margin
const margin = { top: 96 };
A numeric margin that is too small lets the band overlap the body.

To compute a safe margin, call measure() with the same page size and fonts as the render. It lays the band out at full page width with three-digit page counters. Add the 20px inset to the returned height.

Measure a footer margin
import {  } from "@takumi-rs/helpers";
import { ,  } from "takumi-pdf";
import { ,  } from "takumi-pdf/primitives";

const  = await (["Inter"]);
const  = (
  < ={{ : "Inter", : 10, : "center" }}>
    Page < /> of < />
  </>
);

const {  } = await (, { : "a4",  }); 
const  = await (, {
  : "a4",
  ,
  ,
  : { :  + 20 }, 
});

Passing viewport instead of a page size measures the tree as-is. Counter hooks stay empty.

Page counters

<PageNumber /> is the current page, counting from 1, and <TotalPages /> is the number of pages in the document. Both take a format prop for a counter style, and pass className, style, and tw through.

Each primitive renders a <span> with a class hook. A node with the pageNumber or totalPages class receives the counter text in place of its children, so an empty <span /> is enough. Chromium's print templates use the same two class names. In HTML input, write the classes directly:

Page counters in HTML
Page <span class="pageNumber"></span> of <span class="totalPages"></span>

A node with both classes gets the page number.

Counters outside a band

Counters also work in document content. A position: fixed box repeats on every page, so a <PageNumber /> inside it shows each page's number:

Page counters in a fixed box
<div style={{ position: "fixed", bottom: 24, left: 24, right: 24 }}>
  Page <PageNumber /> of <TotalPages />
</div>

A fixed box draws over the content, not in the margin, so leave room for it. Automatic margins fit only the header and footer.

In ordinary flow, <PageNumber /> shows the page the counter lands on. Its value depends on pagination, so the document lays out again after the numbers are filled in.

Counter styles

The format prop names a CSS @counter-style. On a class hook, add the style name as another class. The first supported name wins, unsupported names are ignored, and a counter without one uses decimal.

Format page numbers
import {  } from "takumi-pdf";
import { ,  } from "takumi-pdf/primitives";

const  = await (, {
  : (
    < ={{ : 12 }}>
      第 < ="trad-chinese-informal" /> 頁,共{" "}
      < ="trad-chinese-informal" /> 頁
    </>
  ),
});

Numbers and numerals

StylePage 7Page 12
decimal (default)712
decimal-leading-zero0712
lower-romanviixii
upper-romanVIIXII
cjk-decimal七一二
trad-chinese-informal七十二
cjk-ideographic七十二

Blink defines cjk-ideographic as an extension of trad-chinese-informal, and takumi follows it. Both write numbers up to 9999 in words. Past that they fall back to cjk-decimal, so page 10000 formats as 一零零零零.

Alphabets

These styles count the way a spreadsheet names its columns. The letter after z is aa.

StylePage 12
lower-alpha, lower-latinl
upper-alpha, upper-latinL
lower-greekμ
hiraganaし
katakanaシ

Other digits

These styles write the decimal number in another script's digits.

StylePage 12StylePage 12
arabic-indic١٢mongolian᠑᠒
bengali১২myanmar၁၂
cambodian១២oriya୧୨
devanagari१२persian۱۲
gujarati૧૨tamil௧௨
gurmukhi੧੨telugu౧౨
kannada೧೨thai๑๒
khmer១២tibetan༡༢
lao໑໒urdu۱۲
malayalam൧൨

khmer and cambodian share their digits, as do persian and urdu.

Register a font that covers the digits of the style you pick. Latin fonts rarely include Thai or Tibetan digits.

Page-margin boxes in CSS

A document can draw its own headers and footers with the page-margin rules of CSS Paged Media. Nest one in an @page rule and give it content, and it draws in that part of the page's margin on every page the rule matches.

A running header and a page number
@page {
  margin: 20mm 15mm;
  font-size: 9pt;
  color: #475569;

  @top-left {
    content: "Quarterly report";
  }
  @bottom-center {
    content: "Page " counter(page) " of " counter(pages);
  }
}
@page :first {
  @top-left {
    content: none;
  }
}

There are 16 boxes: three along each edge, such as @top-left, @top-center, and @top-right, and one in each corner, such as @bottom-right-corner. A box without content, or with content: none, is not drawn. The boxes inherit from the @page rule, so font and color set there apply to all of them.

content takes strings, images, counter(page), and counter(pages), with a counter style as the second argument, such as counter(page, upper-roman). counter-increment: page 2 in the @page rule changes how far the page number moves on each page. Other counters read 0.

The three boxes on an edge share its length by the width of their content, and width, height, margin, border, padding, and background size and decorate a box as they do an element. Text is centered across the edge by default, with the outer boxes aligned to the page's sides. text-align and vertical-align change that.

Page-margin boxes draw in the margin the @page rule or the margin option gives, and they do not grow it. They draw alongside a header or footer band, so use one or the other for each margin. A box does not paint the @page rule's own background or border.

Band height

A band with a counter is measured again with the real page count, up to three passes, and automatic margins use the final height. Give counters enough width that a longer page count does not wrap the band to a new line.

Last updated on