Skip to content

Repository files navigation

dom-docx

Convert semantic HTML to native, editable docx files (OOXML): paragraphs, runs, lists, tables, flex and images are all supported.

Live demo: dom-docx.com. Try the converter, browse showcases, read the learn guide.

Built with a visual regression loop: render HTML in Chromium, convert to docx, rasterize via LibreOffice, score layout + structural fidelity against a human-validated metric, iterate. Latest scores: TEST-SCORES.md · methodology: SCORING.md.

Install

npm install dom-docx

Requires Node.js ≥ 20. No browser or Playwright is needed for the default inline path.

When is Playwright needed?

Entry styleSource: "inline" styleSource: "computed" rasterizeInPlace
Node (dom-docx) Pure JS, no browser Playwright + Chromium Playwright + Chromium (same headless page)
Browser (dom-docx/browser) Pure JS, no live DOM Live page: native getComputedStyle Live page: canvas/SVG → PNG <img> in the tab

On Node, playwright is an optional peer dependency. npm install dom-docx pulls only docx, cheerio and fflate, nothing heavy. It is loaded lazily when you pass styleSource: "computed" or rasterizeInPlace. To use those paths, install Playwright and Chromium yourself, once:

npm install playwright
npx playwright install chromium

Playwright is also used by the dev test harness (not required to use the library). Contributors: npm run setup after clone.

LibreOffice is not needed to convert. It is only used for the visual test harness.

Quick start

Node

import { writeFile } from "node:fs/promises";
import { convertHtmlToDocx } from "dom-docx";

const html = `
<h1 style="color:#1a1a2e">Quarterly Report</h1>
<p>Revenue grew <strong>12%</strong> year over year.</p>
<ul>
  <li>North America</li>
  <li>EMEA</li>
</ul>
`;

const docx = await convertHtmlToDocx(html);
await writeFile("output.docx", docx);

Pass a body fragment only (no <!DOCTYPE> / <html> / <body> required). Defaults: US Letter, 1″ margins, Arial 10.5pt body text.

Browser

import { convertHtmlToDocx } from "dom-docx/browser";

const html = `
<h1 style="color:#1a1a2e">Quarterly Report</h1>
<p>Revenue grew <strong>12%</strong> year over year.</p>
<ul>
  <li>North America</li>
  <li>EMEA</li>
</ul>
`;

const blob = await convertHtmlToDocx(html);
// e.g. trigger a download in the browser
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "output.docx";
a.click();

No Playwright, no Node. This runs entirely in the user's tab. See Browser bundle below.

CLI

Convert a file without writing any code:

npx dom-docx input.html -o output.docx
npx dom-docx input.html                      # writes input.docx next to it
cat fragment.html | npx dom-docx - -o -      # stdin → binary stdout (pipelines)
npx dom-docx input.html -s computed          # stylesheet/class HTML (needs playwright installed)
npm install -g dom-docx                      # optional: install globally, then run "dom-docx" without npx

Input is a body HTML fragment, same as the API. --help for all options.

v0.1.x capability

Supported (default styleSource: "inline"):

  • Headings, paragraphs, lists (<ul>/<ol> including list-style-type), tables, links, inline formatting
  • Block backgrounds, blockquotes, <hr>, simple flex rows (≤4 items)
  • data: images; remote images via your imageResolver
  • Page size/orientation/margins, default font, metadata, header/footer HTML, page numbers, table of contents, lang/direction
  • Low-complexity inline SVG (bars + text)
  • CSS bar divs in table cells (background + height/width → native shaded bands)

Advanced (optional styleSource: "computed"):

  • Resolves <style> blocks and class/#id selectors via getComputedStyle
  • Node: requires playwright (optional peer dependency, installed separately) + Chromium. The library launches headless Chromium to render the fragment
  • Browser bundle: uses the live DOM in the user's tab. No Playwright, no extra install
  • SPA fragment export: pass root (browser) or rootSelector (Node + live page) when converting element.innerHTML so computed-style paths match the fragment tree
  • Inline is the supported default for npm installs; computed is for stylesheets/classes or when you already have a rendered page

Charts & complex SVG (optional rasterizeInPlace):

  • Rasterizes <canvas> and complex <svg> (e.g. Highcharts) to PNG <img> before conversion
  • Recommended for charts: rasterizeInPlace: { scale: 2 } — supersamples at 2× density for sharper images in Word (default scale: 1; max 4)
  • Browser: requires root. Clones off-screen by default so the live page is not mutated
  • Node: uses the same Playwright/Chromium context as computed styles; ephemeral spawn pages mutate in place by default
  • Simple inline SVG (rect + text bar charts) still converts natively without rasterization

Not supported in v0.1.x:

  • External stylesheets on the inline path (use computed or inline all styles)
  • Web fonts, CSS grid/float layout, forms, <pre> polish, <dl>, table rowspan
  • Header/footer first/even page variants; guaranteed multi-page layout fidelity
  • Complex SVG (paths, gradients, <use>) unless rasterizeInPlace is used on a live rendered page

See AGENTS.md for HTML authoring tiers and API.md for full options.

API

convertHtmlToDocx(html, options?)

Returns Promise<Buffer> (Node) with a valid .docx file.

import { convertHtmlToDocx, type ConvertOptions } from "dom-docx";

const docx = await convertHtmlToDocx(html, {
  pageSize: "a4",
  orientation: "landscape",
  margins: { top: 0.75, bottom: 0.75 }, // inches; omitted sides default to 1
  defaultFont: { family: "Georgia", sizePt: 11 },
  metadata: { title: "Q3 Report", creator: "Finance" },
  headerHtml: "<p style='font-size:12px;color:#666'>Confidential</p>",
  footerHtml: "<p style='font-size:12px'>© 2026 ACME</p>",
  pageNumber: true,
  lang: "en-US",
  direction: "ltr",
  coverHtml: "<h1 style='text-align:center'>Quarterly Review</h1>", // page 1, before the TOC
  tocHtml: "<ol><li><a href='#intro'>Introduction</a></li></ol>", // your TOC; links jump to id="intro"
});

Options

Option Default Description
styleSource "inline" "inline" parses style="" only (pure JS, fast). "computed" uses getComputedStyle. On Node this requires Playwright + Chromium; in the browser bundle it reads from the live DOM (no Playwright).
browser / page Node only. Reuse an open Playwright browser or page (computed styles and/or rasterizeInPlace). Not used by dom-docx/browser.
rootSelector Node only. CSS selector for the export root when converting element.innerHTML from a live Playwright page. Must match the node whose HTML you pass.
rasterizeInPlace Rasterize <canvas> / chart <svg> to PNG <img> before conversion. Recommended: { scale: 2 } for chart exports. Node: Playwright page; browser: requires root. See API.md.
imageResolver Hook to fetch non-data: <img src> (library never fetches on its own).
onWarning console.warn Called when output silently degrades — unresolved images, or class/stylesheet CSS ignored on styleSource: "inline". Pass null to suppress.
pageSize "letter" "letter", "a4" or { width, height } in inches.
orientation "portrait" "landscape" swaps dimensions.
margins 1 inch each Per-side overrides in inches.
defaultFont Arial 10.5pt { family, sizePt } for body text without explicit CSS.
metadata title, subject, creator, keywords[], descriptiondocProps/core.xml.
headerHtml / footerHtml HTML fragments for page header/footer.
pageNumber false Appends page-number paragraph to footer (truePage {page}; string templates support {page} / {pages}).
lang / direction Spell-check locale; "rtl" for right-to-left.
coverHtml HTML fragment rendered as a cover page — the first content, before the TOC, followed by an automatic page break. Inline styles + data: images (e.g. a logo). Header/footer/page number are suppressed on the cover page.
tocHtml HTML fragment rendered as a table-of-contents "slot" — placed after the cover, before the body. You control the markup/styling (numbered, boxed, columns…); in-page links (<a href="#id">) jump to the matching id in the body. Add a trailing <div style="break-after:page"></div> to put it on its own page.

Images

Only data: URLs embed automatically. For http(s): or file paths, supply a resolver. You control fetch policy and security:

const docx = await convertHtmlToDocx(html, {
  imageResolver: async (src) => {
    const res = await fetch(src); // your allowlist / SSRF checks
    if (!res.ok) return null;
    return { data: new Uint8Array(await res.arrayBuffer()), type: "png" };
  },
});

Browser bundle

For client-side conversion (returns a Blob). No Playwright. The bundle runs entirely in the user's browser.

import { convertHtmlToDocx } from "dom-docx/browser";

// Inline styles: pass HTML string directly
const blob = await convertHtmlToDocx(htmlFragment, { styleSource: "inline" });

// Computed styles: render the fragment in the live DOM first, then convert
document.body.innerHTML = htmlFragment;
const blob2 = await convertHtmlToDocx(htmlFragment, { styleSource: "computed" });

// SPA export: pass the live root so computed paths match element.innerHTML
const root = document.querySelector(".page-body")!;
const blob3 = await convertHtmlToDocx(root.innerHTML, {
  styleSource: "computed",
  root,
});

// Charts (Highcharts, canvas): rasterize to PNG <img> on a clone, then convert
const blob4 = await convertHtmlToDocx(root.innerHTML, {
  styleSource: "computed",
  root,
  rasterizeInPlace: { scale: 2 }, // recommended for charts; default scale is 1
});

Browser-only options: root, document, rasterizeInPlace. Build: npm run build:browserdist/browser/dom-docx.browser.js.

For advanced usage (buildDocxBuffer, custom StyleResolver, engine architecture), see API.md.


What converts well

Optimized for Word-friendly semantic HTML: headings, paragraphs, lists, data tables, inline formatting, shaded callouts, simple flex rows.

Excellent Good Avoid (or use rasterizeInPlace)
Headings, lists, simple tables Shaded banners, flex (≤4 items) Live chart libraries (Highcharts, canvas) unless rasterized
Inline strong / em / links Table row/cell backgrounds Complex SVG paths/gradients unless rasterized
Explicit page breaks (break-before: page, break-after: page) CSS grid, floats, absolute layout
Short span highlights Blockquotes, <hr> External stylesheets (inline path)
Simple SVG bars (rect + text) data: images Forms, web fonts, CSS grid/float layout

Full authoring guide for agents: AGENTS.md.

If a document renders wrong, please open an issue with the HTML that caused it and a quick investigation will ensue.


How it was built

dom-docx maps a practical HTML subset to native OOXML through a three-stage pipeline:

  1. Style resolution: inline style="" (default) or browser computed styles
  2. Visitor: Cheerio walk → docx paragraphs, tables, numbering, hyperlinks
  3. Pack + patch: generate OOXML, patch list numbering and shaded-block alignment for LibreOffice/Word PDF export

Quality is driven by an autonomous loop rather than one-off visual checks:

  • 30+ regression cases (defined in tools/generator.ts, run via npm run score:suite): human-validated layout fidelity (ink-projection structure comparison, 85.6% concordance with blind human quality ratings), plus guards for bad contrast, missing list markers, wrong text and imbalanced shaded blocks; raw pixel match is recorded as a regression tripwire
  • Engine score: 50% visual (layout-based) + 35% editability (native structure, not 1×1 layout tables) + 15% compile speed
  • OSS benchmark: same harness scores html-to-docx and @turbodocx/html-to-docx for ongoing comparison (BENCHMARK.md)

The default inline path is pure JavaScript (docx + cheerio + fflate) with no browser required. Playwright is Node-only: for styleSource: "computed" on the server and for the dev harness. The dom-docx/browser bundle never uses Playwright; computed styles come from the live page's getComputedStyle.

Full scoring formulas, subscores, calibration and the agent iteration workflow: SCORING.md.


Development

For contributors and harness runs (not required to use the library):

git clone … && npm install && npm run setup
npm run build              # dist/ for npm pack
npm run typecheck
npm run score:suite          # full visual + XML regression suite (cases: tools/generator.ts)
npm run score:suite:priority # fast subset of the same cases
# Run one case: SUITE_ONLY=rasterize-in-place-chart npm run score:suite
npm run score:benchmark     # vs html-to-docx + TurboDocx
npm run guard:config        # ConvertOptions OOXML checks

Prerequisites for the harness: LibreOffice (soffice) for PDF rasterization, Playwright Chromium (npm run setup).


Documentation

Doc Contents
API.md Full API reference, engine architecture, usage patterns
AGENTS.md For AI agents: HTML that converts well, what to avoid, plus copyable document shapes
SCORING.md Validation methodology and engine score
TEST-SCORES.md Latest suite metrics and per-case scores
BENCHMARK.md Comparison vs OSS html-to-docx libraries
examples/ Sample HTML, DOCX output and side-by-side previews
SHOWCASE.md How to run and extend showcase examples
CONTRIBUTING.md Library vs harness layout, dev setup

Related projects

  • canvas-word — a Word-style editor for the browser that can open and edit the .docx files dom-docx generates. Canvas-rendered, page-accurate, MIT. Live demo.
  • hmr-serve — point it at any folder and review HTML, Markdown, images, and more live in the browser with Vite HMR (file tree + content pane, or --no-nav like serve).

License

MIT

About

Convert semantic HTML fragments to native, editable Word documents (OOXML)

Topics

Resources

Contributing

Stars

474 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages