Read, edit, and write Word documents in TypeScript

Build a .docx from nothing, or open one that already exists and change it. Paragraphs, lists, tables, images, headers, comments, and tracked changes are plain typed functions, and whatever the library does not model is written back untouched. It runs in Node and in the browser.

Get started Open the playground

A document and the code that built it

hero-document.ts
export function buildHeroDocument(): Docx {
  const doc = createDocx({ paragraphs: [] });
  setPageSize(doc, PAGE_SIZE_A4);
  setPageMargins(doc, MARGINS_NORMAL);
  ensureHeadingStyles(doc);

  appendHeading(doc, "Quarterly review", 1);
  const intro = appendParagraph(doc, "Revenue grew 23% on last quarter. ");
  appendTextRun(intro, "Churn hit a record low.", { bold: true });

  appendHeading(doc, "Highlights", 2);
  addBulletList(doc, [
    "Shipped the self-serve onboarding flow",
    "Cut median support response time to 2 hours",
    "Opened the Osaka office",
  ]);

  appendHeading(doc, "Numbers", 2);
  const table = addTable(doc, [
    ["Metric", "Q2", "Q3"],
    ["Revenue", "$1.20M", "$1.48M"],
    ["Customers", "412", "497"],
    ["Churn", "3.1%", "2.4%"],
  ]);
  setTableBorders(table, { color: "BFC5D2" });
  const header = table.rows[0]!;
  setTableRowAsHeader(header);
  for (const cell of header.cells) setTableCellShading(cell, { fill: "E9EFFC" });

  return doc;
}

Quarterly review

Revenue grew 23% on last quarter. Churn hit a record low.

Highlights

  • Shipped the self-serve onboarding flow
  • Cut median support response time to 2 hours
  • Opened the Osaka office

Numbers

MetricQ2Q3
Revenue$1.20M$1.48M
Customers412497
Churn3.1%2.4%

That page is real output. The code shown with it built the document; this site saved it as .docx bytes, opened those bytes again with openDocx, and drew what it read as HTML. Download the file and open it in Word: the headings are styles, the list is a list, and the table is a table.

  • createDocx()

    Start from an empty document

    createDocx() returns a document with its styles part and relationships already in place, so there is no template file to ship. Append headings, paragraphs, bullet and numbered lists, tables, inline images, headers, footers, footnotes, and a table of contents.

    Write one in the REPL
  • openDocx(bytes)

    Edit a file you already have

    Open a .docx, replace text across the body, headers, footers, footnotes, endnotes, and comments, swap an image by its alt text, accept or reject tracked insertions and deletions, and save. Whatever the library does not model goes back into the file as it came.

    Browse the recipes
  • previewToDOM(doc, element)

    Show the document in the page

    The companion @office-kit/docx-preview package renders a Docx, or raw bytes, into a DOM element. It wraps the open-source docx-preview renderer behind one function. It runs in the browser, so the file is never uploaded.

    Try it in the playground

Design the template in Word. Fill it in from code.

The letterhead, the contract, the report with the right fonts: most documents that matter already exist as a .docx somebody cares about. Open it, replace what changes, and everything else stays as it was designed.

Word often splits a placeholder such as {{name}} across several runs. Matching works on the paragraph’s text rather than its XML, so it is still found, and replaceTextEverywhere reaches the headers, footers, notes, and comments as well as the body.

template-fill.ts
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));

Round trips, checked by machines

A library that edits other people’s documents has one job above all: do not break them. This is what backs that up.

Real Word files go in and come out the same.
The test suite opens the .docx corpora that mammoth.js and python-docx test against, saves every file, reopens it, and compares the paragraphs and the text. CI runs it on Node 22 and Node 24 for every pull request.
What the library does not understand, it does not touch.
Elements it does not model are kept as pass-through nodes at their original position, and parts it does not read stay in the package. A content control or a custom XML part survives an edit.
validate() looks for what makes Word offer a repair.
Relationships that point at a missing part, comment, footnote and endnote references with no target, unpaired bookmarks, missing media. Twenty feature combinations are asserted clean in the test suite.
You ship only the functions you import.
120 side-effect-free exports and no classes. createDocx, appendParagraph, and toUint8Array bundle to 42 KB minified; the whole API is 133 KB. CI fails if the small bundle passes 50 KB.
One runtime dependency.
fflate, for ZIP. The XML parser and serializer are written here, and nothing in the library imports a Node built-in.

How it differs from the docx package

The docx package is the established way to generate a Word file in JavaScript, and it can author things this library cannot yet. The difference is direction: that package builds new documents, while this library also reads and edits the ones you already have. Compared against docx 9.7.1.

Capability@office-kit/docxdocx (npm)
Open and edit an existing .docxYes. The file becomes a model: change any paragraph, run, or tablePlaceholders only. patchDocument swaps {{tokens}} for new content
Read back what is in a fileText, outline, statistics, images, fields, hyperlinks, bookmarksNo. patchDetector lists the placeholders and nothing else
What a placeholder can becomeTextText, paragraphs, tables, or images
Tracked changesAccepts or rejects the ones in a file; cannot author new onesAuthors insertions and deletions; cannot resolve existing ones
Merged cells, floating images, text boxesNot yetYes
API shapeTree-shakeable functions over plain data, ESM onlyA tree of class instances; ESM, CommonJS, and script-tag bundles
Runtime dependenciesOne: fflateSix, including jszip and xml-js

Pick the docx package if you only ever generate new documents and need merged cells, floating images, text boxes, or a CommonJS build. Pick this library if the document already exists and you need to read it, change it, or check it.

What you can build today

The library is pre-1.0 and says so. This is what works now, and what does not yet. The API reference lists every function.

Text
Paragraphs, headings, runs with bold, italic, underline, colour, highlight, size, and fonts. Alignment, indents, spacing, borders, shading.
Lists and styles
Bullet and numbered lists. Add, remove, and look up styles, or lift the whole style table from a designed template.
Tables
Rows, cell text, borders, cell shading, vertical alignment, row heights, repeating header rows. Unwrap a table back into paragraphs.
Images
Inline PNG, JPEG, GIF, BMP, TIFF, and SVG. List the images in a file and replace one by part name or alt text.
Page setup
Page size, margins, orientation, section breaks. Headers and footers for default, first, and even pages, with page numbers.
Review
Comments, footnotes, endnotes, bookmarks, external and internal hyperlinks. Accept or reject every tracked insertion and deletion.
Fields
Table of contents, merge fields, and PAGE, NUMPAGES, and DATE fields.
Document
Core and app properties, find and replace across every part, outline, statistics, plain-text extraction, validate().

Not yet

  • Merged table cells and nested tables
  • Floating images, text boxes, and charts (kept when already in a file)
  • A typed API for content controls (kept when already in a file)
  • ISO/IEC 29500 Strict files
  • Rendering to PDF, which is out of scope for good

Readable by AI agents too

Documents are increasingly generated by agents, so the docs are published in a form a model can fetch directly. Every snippet in them is a file that is type-checked against the library when the site builds, so an agent is not handed an API that no longer exists.

  • /llms.txt An index of the docs, for a model to pick from.
  • /llms-full.txt The guides, the recipes, and the API listing in one Markdown file.