Concrete patterns for common workflows. Every snippet imports standalone
functions from @office-kit/docx — there is no Docx class to instantiate;
Docx is just the (data) type returned by createDocx / openDocx.
This shape is what lets the bundler tree-shake any function you don't import.
The snippets are runnable as-is in Node ≥ 20 once the package is installed.
.docx callers tend to mean one of two different things by "template",
and word-kit supports both:
| Styled-base template (PowerPoint style) | Mail-merge template | |
|---|---|---|
| Template file holds | Page setup, header/footer, custom paragraph/run styles, optional cover page. No placeholder text. | Body text with {{placeholder}} tokens. Styling is incidental. |
| Filling step | Open the template, then appendParagraph(doc, text, { style: "MyTitle" }) to add new content that inherits the template's design via its style ids. |
Open the template, then replaceText(doc, /\{\{(\w+)\}\}/g, ...) to swap the tokens in place. |
| Content origin | Generated by the program. | Already written in the template. |
| When to reach for it | "Build me a Q3 status report from this data" — output structure differs each time. | "Send out 500 invoices, each with a different name and total." |
| Example below | §3 (open + appendParagraph + style id). |
§2 (run-spanning {{name}} substitution). |
The pnpm sample script writes both kinds into ./samples/:
30-styled-base-*.docx for the first, 20-26 mailmerge-*.docx for the
second.
import {
addBulletList,
addPageNumberFooter,
addTable,
appendHeading,
appendParagraph,
createDocx,
ensureHeadingStyles,
PAGE_SIZE_A4,
setCoreProperties,
setPageSize,
toUint8Array,
} from "@office-kit/docx";
import { writeFileSync } from "node:fs";
const doc = createDocx({ paragraphs: [] });
setPageSize(doc, PAGE_SIZE_A4);
setCoreProperties(doc, {
title: "Quarterly Report",
creator: "Yamada",
created: new Date().toISOString(),
});
ensureHeadingStyles(doc, 2);
appendHeading(doc, "Quarterly Report", 1);
appendParagraph(doc, "This document was generated by word-kit.");
addBulletList(doc, ["Revenue ↑ 12%", "Costs ↓ 3%"]);
addTable(doc, [
["Metric", "Q3"],
["Users", "2,200"],
["MRR", "$24k"],
]);
addPageNumberFooter(doc, "Page ", "");
writeFileSync("out.docx", toUint8Array(doc));This is the workflow you reach for when you want to reuse an
already-designed .docx (header, footer, page setup, custom styles,
optional cover page) and just author fresh content into it. The
template carries no {{placeholder}} text — its job is the design.
import { addBulletList, appendParagraph, openDocx, toUint8Array } from "@office-kit/docx";
import { readFileSync, writeFileSync } from "node:fs";
// Designed by hand in Word and saved as report-template.docx. The file
// already defines the styles we'll reach for ("AcmeTitle", "BodyHeading",
// …) plus the company header, page-number footer, and page setup.
const doc = openDocx(readFileSync("report-template.docx"));
appendParagraph(doc, "Quarterly Status — Q3 2026", { style: "AcmeTitle" });
appendParagraph(doc, "Internal — Engineering", { style: "AcmeSubtitle" });
appendParagraph(doc, "Highlights", { style: "BodyHeading" });
addBulletList(doc, [
"Shipped v2.0 with run-spanning find/replace.",
"Adopted by three internal teams.",
"External beta opens next sprint.",
]);
writeFileSync("q3-status.docx", toUint8Array(doc));Word resolves <w:pStyle w:val="AcmeTitle"/> against the template's
styles.xml at render time — the new content automatically inherits
the title's bold dark-blue 22pt look without your code redefining it.
If your downstream consumer needs a style that the template doesn't
yet have, call addStyle(doc, …) once before appending — that adds a
new entry to the styles part. findStyleIdByName(doc, "見出し 1")
helps when the template uses localised display names.
This is the other "template" pattern: the template is content with holes, and you fill the holes. Use this when the same skeleton goes out to many recipients with only a few values changing.
import { openDocx, replaceTextEverywhere, toUint8Array } from "@office-kit/docx";
import { readFileSync, writeFileSync } from "node:fs";
const tpl = openDocx(readFileSync("invoice-template.docx"));
// Replace across body + headers + footers + comments + footnotes.
const values: Record<string, string> = {
name: "山田太郎",
date: new Date().toLocaleDateString("ja-JP"),
amount: "¥1,200,000",
};
replaceTextEverywhere(tpl, /\{\{\s*(\w+)\s*\}\}/g, (m) => values[m.captures[0]!] ?? "");
writeFileSync("filled.docx", toUint8Array(tpl));replaceTextEverywhere is run-spanning safe: even when Word's
spell-checker fragments {{name}} into two <w:r> runs, the match is
detected and the substitution lands cleanly inside the first run
(inheriting its formatting).
import {
clone,
openDocx,
replaceTextEverywhere,
setCoreProperties,
toUint8Array,
} from "@office-kit/docx";
import { readFileSync, writeFileSync } from "node:fs";
const template = openDocx(readFileSync("template.docx"));
for (const customer of customers) {
const doc = clone(template);
replaceTextEverywhere(doc, /\{\{(\w+)\}\}/g, (m) => customer[m.captures[0]!] ?? "");
setCoreProperties(doc, {
title: `Invoice for ${customer.name}`,
creator: "Acme Billing",
modified: new Date().toISOString(),
});
writeFileSync(`out/${customer.id}.docx`, toUint8Array(doc));
}clone(doc) is a deep copy. Edits do not leak across siblings.
import {
bookmarks,
coreProperties,
findTextEverywhere,
footers,
headers,
images,
openDocx,
outline,
statistics,
} from "@office-kit/docx";
const doc = openDocx(bytes);
statistics(doc);
// { paragraphs: 42, tables: 3, images: 5, comments: 4, footnotes: 2,
// endnotes: 0, headings: 8, words: 1240, characters: 7820,
// charactersNoSpaces: 6480 }
outline(doc);
// [ { paragraph, level: 1, text: "Chapter 1" }, { level: 2, ... } ]
bookmarks(doc); // [{ name, id, paragraph }]
headers(doc); // [{ partName, relId, text }]
footers(doc); // [{ partName, relId, text }]
images(doc); // [{ partName, contentType, data }]
coreProperties(doc); // { title?, creator?, subject?, ... }
// Spot template placeholders before bulk-replacing:
for (const { partName, matches } of findTextEverywhere(doc, /\{\{\w+\}\}/g)) {
console.log(
partName,
matches.map((m) => m.text),
);
}import { acceptAllRevisions, openDocx, toUint8Array } from "@office-kit/docx";
const doc = openDocx(reviewedBytes);
// Keep insertions, drop deletions:
acceptAllRevisions(doc);
// Or: drop insertions, restore deletions:
// rejectAllRevisions(doc);
const cleanBytes = toUint8Array(doc);import { addImage, createDocx, images, openDocx, replaceImage } from "@office-kit/docx";
import { readFileSync } from "node:fs";
const doc = createDocx({ paragraphs: ["Logo:"] });
const logo = readFileSync("logo.png");
addImage(doc, logo, {
widthEmu: 914400,
heightEmu: 914400,
altText: "Company logo",
});
// Replace an existing image in a template without redoing rels:
const tpl = openDocx(readFileSync("template.docx"));
const firstImage = images(tpl)[0];
if (firstImage) {
replaceImage(tpl, firstImage.partName, readFileSync("new-logo.png"));
}import { validate } from "@office-kit/docx";
const doc = buildSomeDocument();
const issues = validate(doc);
if (issues.some((i) => i.level === "error")) {
console.error("Refusing to save — would produce a docx that Word repairs:");
for (const issue of issues) console.error(` [${issue.level}] ${issue.code} ${issue.message}`);
process.exit(1);
}import { fromBlob, replaceText, toBlob } from "@office-kit/docx";
fileInput.addEventListener("change", async () => {
const file = fileInput.files?.[0];
if (!file) return;
const doc = await fromBlob(file);
replaceText(doc, "{{name}}", "Alice");
const blob = toBlob(doc);
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "filled.docx";
a.click();
URL.revokeObjectURL(a.href);
});