dom-docx: HTML to Editable Word for Real (No Screenshots, No 1×1 Tables)
Every developer who’s ever had to generate a Word document from code hit the same wall: the HTML-to-docx libraries in the OSS ecosystem don’t produce real, editable Word structure. You get something that looks fine when you open it, but try to edit it for real and you discover it’s a screenshot disguised as a .docx, or a table with a single 1×1 cell pretending to be a page layout.
dom-docx is a new, MIT-licensed library that set out to fix exactly that problem — and the story of how it got built is worth telling on its own.
The Boring Problem Nobody Solved Well
From the author’s own framing, in the Show HN launch: a lot of backend document generation work, and updating templates and code for it is one of the least favorite parts of the job — cryptic errors, rebuild loops over a minute long. Wanted to build reports in HTML rendered by JS (Vue, React) instead, but every existing HTML-to-docx option in the OSS space fell short at producing valid, editable Word output.
This is a “MarkItDown-shaped” problem: unglamorous, universal, and something almost every developer-facing product eventually needs — invoices, reports, contracts, exportable dashboards. The dom-docx pitch isn’t a new category of tool. It’s the boring conversion done right, in an npm install.
How It Actually Got Built
This is the part that makes it more than “another converter.” The author applied what he calls Karpathy’s “Autoresearch” pattern — an agent runs iterations against an objective score, keeps what improves, discards what doesn’t — to the OOXML fidelity problem.
The loop works like this: render HTML and screenshot it, convert it to .docx with dom-docx, rasterize that .docx via LibreOffice and screenshot it, score the two screenshots against each other for layout fidelity, feed that score back into the loop, repeat. The author ran this against 37 real-world HTML patterns known to break converters — nested lists, tables, flex layouts, blockquotes — and iterated until the output held up.
Worth flagging clearly: the resulting numbers are the author’s own benchmark, scored with his own harness, not an independently audited result. The published methodology reports 85.6% concordance with blind human evaluations on layout fidelity (using a structure comparison by ink projection), plus guardrails for poor contrast, missing list markers, incorrect text, and unbalanced shaded blocks. The overall “engine score” is weighted 50% visual fidelity, 35% editability (real document structure, not layout tables), 15% compile speed. The full methodology and head-to-head comparison against html-to-docx and @turbodocx/html-to-docx are published in BENCHMARK.md and SCORING.md in the repo, if you want to review the author’s work yourself.
What It Actually Does
Underneath, it’s a three-stage pipeline:
- Style resolution — either inline
style=""attributes (the default, pure JS) or computed browser styles viagetComputedStyle. - Visitor — a traversal with Cheerio that converts HTML into
docxparagraphs, tables, numbering, and hyperlinks. - Pack + patch — generates the OOXML and patches list numbering and shaded block alignment so both LibreOffice and Word render it correctly, including PDF export.
The default path needs nothing more than docx, cheerio, and fflate — no headless browser, no Playwright. That only comes into play if you opt for computed styles or graphic rasterization.
Installation and Usage
npm install dom-docx
Requires Node.js ≥ 20.
CLI, no code needed:
npx dom-docx input.html -o output.docx
npx dom-docx input.html # writes input.docx alongside
cat fragment.html | npx dom-docx - -o - # stdin → stdout binary, for pipelines
npx dom-docx input.html -s computed # HTML with stylesheet/classes, needs Playwright installed
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);
Browser, no Playwright, no Node — runs entirely in the user’s tab:
import { convertHtmlToDocx } from "dom-docx/browser";
const blob = await convertHtmlToDocx(html);
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "output.docx";
a.click();
Input is a body HTML fragment — you don’t need <!DOCTYPE>, <html>, or <body> wrapper. Defaults are US Letter, 1" margins, Arial 10.5pt.
What Converts Well (and What Doesn’t Yet)
| Excellent | Good | Avoid (or use rasterizeInPlace) |
|---|---|---|
| Headings, lists, simple tables | Shaded banners, flex rows (≤4 items) | Live charting libraries (Highcharts, canvas) without rasterizing |
strong/em/inline links |
Table row/cell backgrounds | Complex SVG with paths/gradients without rasterizing |
| Explicit page breaks | Blockquotes, <hr> |
CSS grid, floats, absolute layout |
| Simple SVG bars (rect + text) | data: images |
Forms, web fonts, external stylesheets in the inline path |
For complex graphics and SVG, rasterizeInPlace: { scale: 2 } rasterizes them to PNG before conversion — recommended for anything built with Highcharts or <canvas>.
The API also supports page configuration (pageSize, orientation, margins), metadata, header/footer HTML, page numbers, a cover page (coverHtml), and a table of contents slot (tocHtml) with working internal links. The full options reference is in API.md.
Licensing
MIT, that simple. No paid tier, no hosted API yet (though the project site mentions one on the roadmap).
Is It Worth Using?
If you ever shipped a “export to Word” feature by screenshotting a page and converting it to an image, or faking a layout with a table, this is worth trying. It’s early — v0.1.x, two-day-old release, this author’s first open source project — so expect the gaps mentioned above (no web fonts yet, no CSS grid, no rowspan) rather than a finished product. But the central claim — real OOXML and editable instead of a screenshot pretending to be a document — is exactly the boring problem worth solving once and forgetting about.
Anyone already tried it generating reports from React or Vue? Tell us how it went in the comments. ![]()