Getting started

@office-kit/docx is a library of plain functions. Every operation is a standalone export that takes a Docx value as its first argument, and nothing is hidden behind a class instance. That keeps the package tree-shakeable and makes it work the same way in Node and in the browser.

Install

pnpm add @office-kit/docx @office-kit/docx-preview

@office-kit/docx is the authoring API. @office-kit/docx-preview is an optional companion that mounts a read-only preview of any Docx value into a DOM container. Both ship as ESM with bundled .d.ts types, and neither depends on a Node built-in.

Build a document from scratch

createDocx hands back a plain Docx object that the rest of the API treats as a value. Append to it, then call toUint8Array for the bytes of the file, or toBlob in the browser.

site/src/lib/examples/from-scratch.ts
// Build a .docx from scratch. This file is imported as ?raw into the
// docs site so the snippet shown to readers is exactly what svelte-check
// type-checked — if an API rename breaks it, the docs build fails.

import {
  addBulletList,
  addTable,
  appendHeading,
  appendParagraph,
  createDocx,
  PAGE_SIZE_A4,
  setPageSize,
  toUint8Array,
} from "@office-kit/docx";

const doc = createDocx({ paragraphs: [] });
setPageSize(doc, PAGE_SIZE_A4);

appendHeading(doc, "Quarterly review", 1);
appendParagraph(doc, "Highlights from Q3.");
addBulletList(doc, ["Shipped preview", "Round-trip stable", "Docs site live"]);

addTable(doc, [
  ["Metric", "Q2", "Q3"],
  ["MRR", "$120k", "$148k"],
  ["Churn", "3.1%", "2.4%"],
]);

const bytes: Uint8Array = toUint8Array(doc);
void bytes;

Open a template and fill placeholders

An existing .docx can be opened with openDocx and edited in place. Every XML element the library does not model is kept as a pass-through node at its original position, so saving a template you did not change does not rewrite it into something else.

replaceTextEverywhere walks the body, headers, footers, footnotes, endnotes, and comments, not just the main document. A match such as {{name}} that Word split across several runs of one paragraph is still found; a match that spans two paragraphs is not.

site/src/lib/examples/template-fill.ts
// Open an existing .docx as a template, run cross-part placeholder
// substitution, write it back. The interesting bit: replaceTextEverywhere
// also walks headers, footers, footnotes, endnotes and comments — not just
// the body — so {{name}} in a header swaps out too.

import { readFile, writeFile } from "node:fs/promises";
import { openDocx, replaceTextEverywhere, toUint8Array } from "@office-kit/docx";

const values: Record<string, string> = { name: "Ada Lovelace" };

const doc = openDocx(await readFile("letter-template.docx"));

const replaced = replaceTextEverywhere(
  doc,
  /\{\{(\w+)\}\}/g,
  (m) => values[m.captures[0] ?? ""] ?? "",
);
console.log(`Filled ${replaced} placeholders.`);

await writeFile("letter.docx", toUint8Array(doc));

Render in the browser

@office-kit/docx-preview mounts a read-only preview of a Docx, or of raw bytes, into a DOM container. It wraps the open-source docx-preview renderer behind one function, previewToDOM.

Try it in the playground, or read the recipes for the embedding pattern.

Where to go next

  • Recipes: common scenarios you can copy.
  • API reference: every public export, grouped by area.
  • Playground: drop in a .docx and see the preview.
  • REPL: edit code and watch the document it builds.
  • GitHub: source and issues.