Skip to main content
Version: Canary 🚧

Layouts

A layout is the page master, written as plain data: paper, margins, fold and punch marks, stationery, the envelopes it is designed for, and named areas that host parts. invoice-pro draws the page frame itself, so any format is a dictionary, from DIN 5008 to an 80 mm receipt roll.

Built-in Layouts​

All layouts live in theme.layout. Window layouts list the envelopes they were checked against (see Envelopes and Print Proofs).

LayoutPaperWindowEnvelopes (declared)TierDefault of
din-5008-aA4left, DIN 5008 zoneDIN DL, C6/5, C5 form A, C4 form AstableDE and unknown regions
din-5008-bA4left, DIN 5008 zoneDIN DL, C6/5, C5 form BstableAT; elegant where A is picked
a4-window-rightA4rightFR DL and C5, IT 11Γ—23, ES americano (two positions)experimentalFR, IT, ES
a4-window-leftA4left, no return-address zoneUK DL (BS 4264), a DL variant, C5experimentalGB
us-letter-10Letterleft, #10 envelopeUS #10experimentalUS
sn-010130-rightA4right, SN 010130CH C5/6 and C5 with a right windowexperimentalCH
sn-010130-leftA4left, SN 010130CH C5/6 (DIN position) and C5 with a left window, DIN DLexperimental–
a4-digitalA4none–stablebold, technical, soft
us-letter-digitalLetternone–stablebold, technical, soft in the US
plainA4none; everything in the flow–stableplain
a4-sidebarA4none; 56 mm brand rail–experimentalcorporate
us-letter-sidebarLetternone; 56 mm brand rail–experimentalcorporate in the US
a4-bandA4none; 40 mm full-bleed band–experimentalprestige
us-letter-bandLetternone; 40 mm full-bleed band–experimentalprestige in the US
a4-denseA4none; one header row–experimentalcompact
us-letter-denseLetternone; one header row–experimentalcompact in the US

Every built-in layout with the classic look, identical data, page 1

The Swiss layouts reserve no zone for a QR-bill; see Swiss QR-Bill Zone. The band layouts print to the edge of the sheet, which most office printers cannot do: they are meant for PDF invoices.

Layout by Region​

Without layout:, a preset picks its page master from the sender's country, because paper and envelopes belong to the sender: an English invoice from Germany still goes into a German envelope. The country is taken from sender.country (a value of the country module), else from sender.region, else from the locale's region. An explicit layout: always wins.

#import "@preview/invoice-pro:0.6.0": *

// layout: auto (the default) follows the sender: an Austrian sender gets din-5008-b
#show: invoice.with(
locale: locale.de-at,
sender: (
name: "Kaffeehaus Weber GmbH",
address: "KΓ€rntner Ring 5",
city: "1010 Wien",
country: country.at,
vat-id: "ATU12345678",
),
recipient: (
name: "Muster AG",
address: "Beispielweg 5",
city: "1020 Wien",
),
invoice-nr: "2026-0142",
theme: theme.classic, // pin a page master with theme.classic.with(layout: ..)
)

#line-items[
#item([Catering, 40 guests], price: 1200)
]

The window layout for each region:

Sender's countrytheme.layout.for-region(..)
DE, and unknown onesdin-5008-a
ATdin-5008-b
CHsn-010130-right
FR, IT, ESa4-window-right
GB (also uk)a4-window-left
USus-letter-10

Each preset turns the region into its own layout family:

Presetlayout: auto resolves to
classic, boxedfor-region(region)
elegantfor-region(region), with din-5008-b where that gives din-5008-a
plainplain-for-region(region)
corporatesidebar-for-region(region)
prestigeband-for-region(region)
bold, technical, softdigital-for-region(region)
compactdense-for-region(region)

The region functions are public, so you can use them yourself, for example when a job file names the region:

FunctionReturns
for-region(region, default: din-5008-a)The window layout of the table above; any other region gives default.
paper-for-region(region)"us-letter" for us, else "a4".
digital-for-region(region)us-letter-digital on Letter paper, else a4-digital.
plain-for-region(region)plain on the region's paper.
sidebar-for-region(region)us-letter-sidebar on Letter paper, else a4-sidebar.
band-for-region(region)us-letter-band on Letter paper, else a4-band.
dense-for-region(region)us-letter-dense on Letter paper, else a4-dense.

region is an ISO 3166 code in any case; none and unknown codes fall back.

#let job = (region: "us") // e.g. json("job.json")

#show: invoice.with(
theme: theme.classic.with(
layout: theme.layout.digital-for-region(job.region),
),
// sender: .., recipient: .., invoice-nr: ..
)

Layout Keys​

KeyTypeDefaultDescription
namestr"plain"Shown in error messages.
paperstr | dictionary"a4"A Typst paper name, or (width: length, height: length). height: auto is a continuous roll.
flippedboolfalseLandscape.
margindictionary(top: 20mm, right: 20mm, bottom: auto, left: 25mm)Partial patches fold. bottom: auto is computed from the tallest footer (see below).
header-ascentrelative30%Passed to set page.
footer-descentrelative30%Passed to set page; must stay below 100 %.
footer-clearancelength5mmThe last footer line ends at least this far above the edge of the sheet.
body-topauto | lengthautoWhere the body starts on page 1. auto: below the lowest reserving area plus body-gap.
body-gaplength4.23mmSpace between the reserving areas and the body.
stationerynone | "pre-printed" | dictionarynoneSee Stationery.
marksnone | dictionarynoneFold and punch marks: (fold: array, punch: length or none, left: length, length: length, stroke: stroke). See Marks.
envelopesarray()Envelope records for the fit check and the print proof.
proofbool | arrayfalseThe print-proof overlay; see Envelopes and Print Proofs.
areasdictionarythe standard areasNamed areas, in drawing and reading order. none removes one.

The bottom margin is computed. With margin.bottom: auto (every built-in layout), the bottom margin is the tallest footer plus footer-descent and footer-clearance, at least 20 mm, so the legal footer can never run off the sheet. A one-page invoice is sized for its own footer. An explicit bottom margin that is too small for the footer is a lint/footer-fit issue: a draft marks the overflow, strict stops.

Areas​

An area is a region of the page that hosts parts. Its place decides how it is drawn:

placeDrawn
"fixed"At an absolute position (left/right, top/bottom). On page 1 it is part of the tagged content and pushes the body down.
"before"In the flow, above the body (page 1).
"after"In the flow, below the body.
"header"In the running header of every page that pages selects.
"footer"In the running footer of every page that pages selects. The computed bottom margin makes room for it.
"background"Behind the page content: marks, bands, rails.
"foreground"Above the page content: stamps.

Every layout has the standard areas marks, letterhead, address, info, references, title, continuation, page-number and footer. A layout that does not define one gets an empty stub, so patches that name a standard area work on any layout.

Area Fields​

FieldTypeDefaultDescription
placestr"fixed"See the table above.
pagesauto | "all" | "first" | "rest" | "last" | "not-last"autoauto: "first" for fixed and flow areas, "all" for headers, footers and layers.
left, rightauto | length | ratio | relativeautoHorizontal anchor of a fixed area or a layer. Setting one clears the other.
top, bottomauto | length | ratio | relativeautoVertical anchor. Setting one clears the other; bottom needs a height.
width, heightauto | length | ratio | relativeautoauto: the paper width for fixed areas, the text width in the flow; the content height.
partsarray()Part names, content or functions (ctx, view) => content, in reading order.
arrange"stack" | "row" | dictionary | function"stack"How the cells are arranged; see below.
gaplength | derivationspacing.mediumSpace between the cells.
alignalignmenttop + startAlignment of the area's content.
cell-alignauto | alignment | arrayautoAlignment per cell; an array is indexed by cell, its last entry repeats. Wins over arrange.align.
pardictionary(:)set par arguments for the cells, such as (leading: 0.5em); values may be derivations.
insetlength | dictionary0ptPadding inside the area; partial patches fold.
fillnone | color | gradient | tiling | derivationnoneBackground of the area.
strokenone | stroke | dictionary | derivationnoneBorder of the area.
radiuslength | relative | dictionary | derivation0ptCorner radius of fill and border.
rulenone | dictionary | derivationnoneA line outside the box, (side: top or bottom, stroke:, gap:). It adds no height.
textdictionary(:)set text arguments for the cells, such as (size: 7pt); values may be derivations.
stationeryboolfalseThe area belongs to the letterhead: it is dropped when the layout's stationery is not none.
reserveauto | boolautoWhether the area pushes the body down on page 1. auto: fixed first-page areas and first-page background bands with a height.
isolate, floatboolfalse0.6.x preview: a regulated, brand-immune zone that floats to the bottom edge (used by reserve-qr-bill).

arrange takes these forms:

ValueArrangement
"stack"The cells one below the other, separated by gap.
"row"The cells side by side.
(columns: .., align: ..)A grid with these columns; gap is the gutter.
(rows: .., align: ..)A grid with these rows; gap is the gutter.
(.., skip-empty: true)With columns or rows: an empty cell is left out with its track and its align entry where these list one per cell, so the other cells share the space. The legal footer of a4-digital uses it: without extra, the contact column gives its width to the others.
(ctx, cells, area) => contentExperimental. cells is an array of (name, body), area the (width, height) inside the inset.

An area whose cells are all empty draws nothing, unless it has an explicit height.

A preset's look may patch only the style fields of an area: fill, stroke, text, inset, arrange, gap, align, cell-align, par, radius and rule. Geometry belongs to the layout, so any look works on any layout.

Patching Areas​

theme.custom.area(name, ..fields) patches an area of whatever layout is active. Fields you leave out keep their value.

#show: invoice.with(
theme: theme.classic.with({
import theme.custom: *
brand(logo: image("logo.svg", alt: "Atelier Nord"))
area("letterhead", parts: ("sender", "logo")) // the logo moves to the right
area("references", none) // no reference row above the title ..
area("info", parts: ("sender-details", "reference-list")) // .. the references go next to the address
area("continuation", none) // no header on following pages
area("footer", text: (size: 6.5pt))
}),
// sender: .., recipient: .., invoice-nr: ..
)
CallEffect
area("address", top: 50mm)Moves an existing area; the other fields stay.
area("address", left: 22mm)Sets left and clears right: the window moves to the other side.
area("continuation", none)Removes the area. Removing an area the layout lacks does nothing.
area("stamp", place: "foreground", ..)A patch with place adds a new area (or re-creates a removed one).

A layout patch applies to the final layout, wherever it stands in the chain. For company-wide geometry, derive a layout instead.

Deriving a Layout​

theme.layout.derive(base, ..patches) returns a new layout from an existing one and layout patches. The derived layout keeps the declared envelopes, so the fit check covers the new window position.

// A company-specific window: geometry lives in a derived layout
#let our-window = theme.layout.derive(theme.layout.din-5008-a, {
import theme.custom: *
area("address", left: 24mm, top: 40mm)
area("info", top: 45mm)
page(margin: (bottom: 35mm))
})

#show: invoice.with(
theme: theme.classic.with(layout: our-window),
// sender: .., recipient: .., invoice-nr: ..
)

derive accepts only layout patches (page, stationery, marks, envelopes, proof, area) or dictionaries of the form (layout: (..)).

Marks​

Fold and punch marks are layout data, drawn by the marks part in the marks area.

PatchEffect
theme.custom.marks(none)No marks at all (digital invoices).
theme.custom.marks(punch: none)Fold marks only.
theme.custom.marks(fold: ())Punch mark only.
theme.custom.marks(fold: (105mm, 210mm))Other fold positions; on a layout without marks this adds marks again.

The mark fields are fold (array of lengths from the top), punch (length or none), left (distance from the left edge), length and stroke (default: strokes.hairline in colors.text).

Stationery​

stationery tells the frame what the paper already carries.

ValueEffect
none (default)The theme draws everything.
"pre-printed"Printing on letterhead paper: every area marked stationery: true (letterhead and footer in the window layouts) is dropped.
(first: content, rest: content)Letterhead art under page 1 and the following pages, stretched to the sheet. The stationery: true areas are dropped as well.

One document can serve all three outputs:

#let mode = sys.inputs.at("output", default: "pdf") // print | pdf | einvoice

#show: invoice.with(
theme: theme.classic.with(layout: theme.layout.din-5008-b, {
import theme.custom: *
if mode == "print" { stationery("pre-printed") }
if mode == "pdf" {
stationery((
first: image("letterhead-1.svg"),
rest: image("letterhead-2.svg"),
))
area("continuation", none) // the art of following pages has its own header
}
if mode != "print" { marks(none) }
}),
zugferd: if mode == "einvoice" { "basic" },
// sender: .., recipient: .., invoice-nr: ..
)

The three stationery modes: pre-printed paper, letterhead art, generated letterhead

warning

PDF/A and PDF/UA exports cannot embed PDF images (a Typst limitation). Convert letterhead art to SVG; a PDF image is reported as lint/pdf-image-stationery.

With stationery, the letterhead no longer shows the supplier's name and tax number, so the requirements for them are waived (see What Every Invoice Layout Must Host).

Envelopes and Print Proofs​

A folded sheet moves inside its envelope. The recipient box of every window layout is placed where it shows through the window in every position of the sheet, with 2 mm clearance, for all declared envelopes: at least five lines of 60 mm (four on US #10).

An envelope record:

FieldTypeDescription
namestrUsed in messages and in proof(..).
size(length, length)Outer width and height of the envelope.
windowdictionary(left or right, top or bottom, width, height), measured on the front of the envelope with the flap edge up.
foldauto | arrayFold positions of the sheet. auto: the layout's marks.fold.
notestrOptional: the source of the measurements.

theme.layout.envelope holds 21 records: din-dl, din-c6-5, din-c5-a, din-c5-b, din-c4-a, din-c4-b, ch-c5-6-right, ch-c5-right, ch-c5-6-left, ch-c5-6-left-din, ch-c5-left, fr-dl-right, fr-c5-right, it-11x23-right, es-americano-right, es-americano-right-25, uk-dl, uk-dl-22, uk-c5, us-10 and us-10-5-8. Their values are provisional. theme.layout.folded(folds, ..envelopes) fixes the fold of envelopes that fold with the layout, so the fit stays right when marks(none) removes the printed marks.

PatchEffect
theme.custom.envelopes(..records)Replaces the list of envelopes. A folded sheet that does not fit an envelope is a lint/envelope-<name> issue.
theme.custom.proof(value)true draws every envelope window on every invoice page, in both extreme positions, with the band that always shows, the fold and punch lines, the recipient box and a legend. An array of names limits it to these envelopes.

The proof is meant for one test print that you hold against the real envelope. Tie it to an input, so it never ends up in production output:

// print one sheet with --input proof=1 and hold it against the envelope
#show: invoice.with(
theme: theme.classic.with({
import theme.custom: *
envelopes(theme.layout.envelope.din-dl, (
name: "ours-c6-5",
size: (229mm, 114mm),
fold: (87mm, 192mm),
window: (left: 22mm, bottom: 16mm, width: 90mm, height: 45mm),
))
proof(sys.inputs.at("proof", default: "") == "1")
}),
// sender: .., recipient: .., invoice-nr: ..
)

Proof overlays of din-5008-a, sn-010130-right, a4-window-right and us-letter-10

The recipient box of the proof is green when at least five lines show in every declared envelope, amber when fewer do (four on US #10, by design). The draft report page gets no overlay.

note

The window geometry comes from standards literature and manufacturer data (DIN 680 via DIN 5008 for DE and AT; Elco and INKA for CH; manufacturer data for FR, IT, ES and the UK; USPS for the US). The layouts other than DIN 5008 are experimental until test prints with real envelopes confirm them. Print a proof before a mailing.

Writing Your Own Format​

A new format is a layout dictionary. Keys you leave out take their defaults, and the standard areas you leave out become empty stubs. An 80 mm thermal-roll receipt:

#import "@preview/invoice-pro:0.6.0": *

// Any format is data: an 80 mm thermal-roll receipt (continuous page)
#let roll = (
name: "roll-80",
paper: (width: 80mm, height: auto),
marks: none,
margin: (x: 4mm, top: 6mm, bottom: 16mm),
areas: (
letterhead: (place: "before", parts: ("sender",), align: center),
title: (place: "before", parts: ("title",)),
address: (place: "before", parts: ("recipient",)),
footer: (place: "footer", parts: ("registration",), text: (size: 6pt)),
),
)

#show: invoice.with(
theme: theme.plain.with(layout: roll, theme.custom.sizes(body: 8pt)),
sender: (
name: "CafΓ© Hafenblick",
address: "Kai 1",
city: "24103 Kiel",
vat-id: "DE123456789",
),
recipient: (
name: "Muster AG",
address: "Beispielweg 5",
city: "80331 MΓΌnchen",
),
invoice-nr: "B-0815",
)

#line-items[
#item([Cappuccino], quantity: 2, price: 3.9)
#item([Apple cake], price: 4.5)
]

Left: a third-party A5 landscape layout with a brand rail. Right: an 80 mm thermal roll

A layout can also add areas of its own and host custom parts; third-party packages ship layouts this way (Theme Packages).

What Every Invoice Layout Must Host​

Some parts carry legally required output. The layout must place them:

RoleParts that satisfy itWhereWaived by stationery
titletitlea flow area, or a fixed area drawn on page 1no
recipientrecipientexactly one flow area or fixed area drawn on page 1no
suppliersender, company or return-addressany area drawn on page 1, including headers, footers and layersyes
tax-idregistration, references or reference-listany area drawn on page 1yes

In addition, the invoice number and date must appear in the first-page output; this is checked after layout. A missing role, a missing number or date, or a required part that renders nothing is a theme issue: strict stops, a draft marks it and lists it on the report page, none renders the document as it is. See Validation.

The layout is also checked for accidents: a fixed area with pages: "all" that overlaps the body on following pages (lint/overprint-*), an area that overlaps the address window (lint/window-*) and an envelope that does not take the folded sheet (lint/envelope-*).

DIN 5008 form A as data

The built-in layouts are written the same way. This listing is equal to theme.layout.din-5008-a and theme.layout.din-5008-b:

#let din-5008-a = (
name: "din-5008-a",
paper: "a4",
margin: (top: 20mm, right: 20mm, bottom: auto, left: 25mm), // bottom: computed
marks: (fold: (87mm, 192mm), punch: 148.5mm, left: 5mm, length: 2.5mm),
envelopes: theme.layout.folded(
(87mm, 192mm),
theme.layout.envelope.din-dl,
theme.layout.envelope.din-c6-5,
theme.layout.envelope.din-c5-a,
theme.layout.envelope.din-c4-a,
),
areas: (
marks: (place: "background", parts: ("marks",)),
letterhead: (
left: 25mm,
top: 8mm,
width: 165mm,
height: 19mm,
stationery: true,
par: (leading: 0.5em),
parts: ("logo", "sender"),
arrange: (columns: (1fr, auto), align: (left + horizon, right + top)),
),
address: (
left: 20mm,
top: 27mm,
width: 85mm,
height: 45mm,
inset: (left: 5mm, right: 5mm),
par: (leading: 0.5em),
parts: ("return-address", "recipient"),
gap: 0pt,
arrange: (rows: (17.7mm, 27.3mm), align: (left + bottom, left + top)),
),
info: (
left: 125mm,
top: 32mm,
width: 75mm,
height: 40mm,
parts: ("sender-details",),
),
references: (place: "before", parts: ("references",)),
title: (place: "before", parts: ("title",)),
continuation: (place: "header", pages: "rest", parts: ("continuation",)),
page-number: (place: "footer", parts: ("page-number",), align: right),
footer: (
place: "footer",
stationery: true,
text: (size: t => t.sizes.fine, fill: t => t.colors.text-muted),
par: (leading: 0.45em),
parts: ("company", "contact", "registration", "bank-account"),
arrange: (columns: (1fr, 1fr, 1fr, 1fr)),
),
),
)

#let din-5008-b = theme.layout.derive(din-5008-a, (
layout: (
name: "din-5008-b",
marks: (fold: (105mm, 210mm)),
envelopes: theme.layout.folded(
(105mm, 210mm),
theme.layout.envelope.din-dl,
theme.layout.envelope.din-c6-5,
theme.layout.envelope.din-c5-b,
),
areas: (
letterhead: (height: 37mm),
address: (top: 45mm),
info: (top: 50mm),
),
),
))

The recipient starts 17.7 mm into the address field, as DIN 5008 prescribes, and its text stops at 100 mm, inside a DL window even when the sheet slides sideways.

Swiss QR-Bill Zone (0.6.x Preview)​

sn-010130-right and sn-010130-left reserve no zone for the Swiss QR-bill, so a Swiss invoice stays a normal letter. The second fold at 192 mm is the perforation line of a QR-bill, so a payment part added later is never folded through.

theme.layout.reserve-qr-bill(layout) is an explicit opt-in. It adds a 210 Γ— 105 mm qr-bill area at the bottom of the last page and moves the footer above it.

warning

This is a 0.6.x preview. Until the QR-bill component ships, the zone shows a placeholder slip without a QR code, and it may need a page of its own. It requires A4 portrait paper (lint/qr-bill-paper).

#let sn = theme.layout.sn-010130-right // what layout: auto picks for a Swiss sender
#let qr-bill = sys.inputs.at("qr-bill", default: "") == "1" // opt-in preview

#show: invoice.with(
locale: locale.de-ch,
theme: theme.classic.with(
layout: if qr-bill { theme.layout.reserve-qr-bill(sn) } else { sn },
{
import theme.custom: *
if sys.inputs.at("window", default: "right") == "left" {
area("address", left: 22mm) // setting left clears right
area("info", right: 18mm) // setting right clears left
}
},
),
// sender: .., recipient: .., invoice-nr: ..
)