A powerful, flexible, and production-ready Mail Merge package for Typst. Easily generate personalized letters, certificates, invoices, address labels, conference badges, envelopes, and reports directly from CSV files or inline data structures.
๐ Documentation & Wiki
Explore our complete GitHub Wiki for detailed guides, API reference, preset specs, and examples:
- ๐ Getting Started โ Installation, CSV data loading, and first merge letter setup.
- ๐ Document Mail Merge โ Multi-page documents & page counter resets.
- ๐ท๏ธ Label & Badge Sheets โ Avery sticker presets, grid layout engine, cut lines & cell fills.
- ๐ง Fields & Formatters โ Smart key normalization, currency formatters, and address cleaner.
- ๐ Filtering, Sorting & Pagination โ Filtering conditions, custom sorting, reversing, and batch pagination.
- ๐ API Reference โ Full signature and parameter documentation.
- ๐ก Examples & Templates โ Ready-to-use Typst code templates.
โจ Features
- ๐ Document Mail Merge (
mail-merge): Seamless single or multi-page personalized document generation. - ๐ท๏ธ Label & Badge Sheet Layouts (
mail-merge-labels): Grid layout engine with built-in presets for Avery address/shipping labels, name badges, and card sheets. - ๐ฏ Smart Field Access & Normalization (
field): Normalizes field names (e.g. matching"First Name","first_name","First-Name", or fallback candidate arrays). - ๐งน Address Joining (
join-fields): Cleanly joins address lines, automatically omitting blank optional fields (e.g.,Address 2). - ๐จ Field Formatters (
fmt-field): Quick presets for"upper","lower","title","currency", or custom transform closures. - ๐ Filtering & Sorting: Filter records with functions or dictionary conditions, sort by string fields or custom functions, reverse order, and paginate (
start,limit). - ๐ข Page Counter Management: Resets page numbers per recipient document with
reset-page-counter: true. - ๐ Dataset Statistics & Fast Preview: Inspect fields and record counts with
mail-merge-statsand preview drafts withmail-merge-preview.
๐ Quick Start
1. Simple Letter Merge
#import "@preview/modern-mailmerge:0.1.0": mail-merge, field, fmt-field, join-fields, if-field
#mail-merge(
csv("data/clients.csv", row-type: dictionary),
filter: record => field(record, "Status") == "Active",
sort-by: "Last Name",
reset-page-counter: true,
record => [
#align(right)[#datetime.today().display("[Month repr:long] [Day], [Year]")]
*Dear #field(record, "First Name"),*
Thank you for being a valued customer since #field(record, "Join Date").
#block(
fill: rgb("#f7fafc"),
inset: 10pt,
radius: 4pt,
[
*Recipient Address:* \
#fmt-field(record, "First Name") #fmt-field(record, "Last Name") \
#if-field(record, "Company", c => [#c \ ])
#field(record, "Address 1") \
#join-fields(record, ("City", "State"), separator: ", ") #field(record, "Zip")
]
)
Your outstanding balance is *#fmt-field(record, "Balance", fmt: "currency")*.
Best regards, \
The Acme Team
]
)
2. Address Labels & Badge Sheet
#import "@preview/modern-mailmerge:0.1.0": mail-merge-labels, presets, field, fmt-field, join-fields
// Render 3x10 Avery 5160 Address Sticker Sheet with cut guidelines
#mail-merge-labels(
csv("data/clients.csv", row-type: dictionary),
preset: presets.avery-5160,
show-cut-lines: true,
record => [
#text(size: 9pt, weight: "bold")[#fmt-field(record, "First Name") #fmt-field(record, "Last Name")] \
#text(size: 8pt)[
#field(record, "Address 1") \
#join-fields(record, ("City", "State"), separator: ", ") #field(record, "Zip")
]
]
)
๐ API Reference
mail-merge
Main function for generating one document per record.
#mail-merge(
data,
template,
filter: none,
sort-by: none,
reverse: false,
start: 1,
limit: none,
pagebreak: true,
reset-page-counter: false,
trim: true,
default-value: "",
on-empty: [No matching records found.]
)
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
str | array |
Required | Path to CSV, pre-loaded csv(...), array of dicts, or array of row arrays. |
template |
function |
Required | Content closure record => content. |
filter |
none | function | dict |
none |
Filter closure r => bool or dict (Status: "Active"). |
sort-by |
none | str | function |
none |
Key name or closure r => key to sort records. |
reverse |
bool |
false |
Reverse sort order. |
start |
int |
1 |
1-based start record index for pagination. |
limit |
none | int |
none |
Maximum number of records to process. |
pagebreak |
bool |
true |
Inserts #pagebreak() between rendered records. |
reset-page-counter |
bool |
false |
Resets #counter(page) to 1 for each record. |
trim |
bool |
true |
Trims whitespace from CSV string fields. |
default-value |
str |
"" |
Fallback string for empty or missing CSV fields. |
on-empty |
content |
[...] |
Content displayed if zero records match. |
mail-merge-labels
Engine for multi-column label grids, sticker sheets, and badges.
#mail-merge-labels(
data,
template,
preset: none,
columns: none,
rows: none,
width: none,
height: none,
column-gutter: none,
row-gutter: none,
page-margin: none,
paper: none,
cell-padding: none,
show-cut-lines: false,
fill: none,
..options
)
| Parameter | Type | Default | Description |
|---|---|---|---|
preset |
dictionary |
none |
Preset layout from presets.* (e.g. presets.avery-5160). |
columns |
int |
3 |
Number of grid columns per sheet. |
rows |
int |
8 |
Number of grid rows per sheet. |
width |
length | 1fr |
1fr |
Width of each label cell. |
height |
length | 1fr |
1fr |
Height of each label cell. |
column-gutter |
length |
0pt |
Space between columns. |
row-gutter |
length |
0pt |
Space between rows. |
show-cut-lines |
bool | stroke |
false |
Visual guidelines around labels. |
fill |
none | color | function |
none |
Background fill or closure record => color. |
๐ท๏ธ Label Presets (presets)
Pre-configured grid dimensions for standard label sheets:
presets.avery-5160: 3x10 Address Labels (2.625" x 1.0", Letter)presets.avery-5161: 2x10 Address Labels (4.0" x 1.0", Letter)presets.avery-5162: 2x7 Folder Labels (4.0" x 1.33", Letter)presets.avery-5163: 2x5 Shipping Labels (4.0" x 2.0", Letter)presets.avery-5164: 2x3 Large Shipping Labels (4.0" x 3.33", Letter)presets.avery-l7160: 3x7 Metric Address Labels (63.5mm x 38.1mm, A4)presets.avery-l7163: 2x7 Metric Address Labels (99.1mm x 38.1mm, A4)presets.a4-3x8: 3x8 Standard A4 Labels (70mm x 36mm, A4)presets.a4-2x7: 2x7 Standard A4 Labels (105mm x 42.3mm, A4)presets.badge-2x4: 2x4 Name Badges (3.5" x 2.25", Letter)presets.card-2x2: 2x2 Place Cards / Large Badges (3.75" x 4.5", Letter)
๐ง Utility Functions
field(record, key, fmt: none, default: ""): Retrieves and optionally formats a field value with smart key normalization (matches"First Name","first_name","First-Name"or candidate array("FirstName", "Name")). Supportsfmt: "upper","lower","title","currency", or custom function.bind-field(record): Binds a record tofieldfor ultra-concise#f("Field Name")template syntax (let f = bind-field(record)orlet f = field.with(record)).fmt-field(record, key, fmt: none, default: ""): Alias forfield(record, key, fmt: fmt, default: default).join-fields(record, keys, separator: ", ", default: ""): Joins multiple non-empty fields, automatically omitting blank optional lines.if-field(record, key, then-content, else-content: []): Conditionally rendersthen-contentwhen field is present and non-empty.is-empty(record, key)/is-non-empty(record, key): Helper predicates for conditional checks.record-index(record)/record-total(record): Returns 1-based current record index and total records count.is-first-record(record)/is-last-record(record): Boolean flags for first/last record in merged set.mail-merge-stats(data): Returns(total-records: int, fields: array, sample-record: dict).mail-merge-preview(data, template, limit: 3): Convenience wrapper for rapid draft rendering.
๐ Repository Examples
Check out the examples/ directory for full working .typ templates:
examples/letter_merge.typโ Personalized letters and invoices.examples/certificate_merge.typโ Landscape certificate of completion templates.examples/label_merge.typโ Address sticker sheets and conference name badges.examples/envelope_merge.typโ DL envelope printing.examples/advanced_features.typโ Advanced sorting, filtering, statistics, and inline array data.
๐ License
Distributed under the MIT License.