Learn dom-docx

Copy-paste examples for the most common integration paths. Pass a body HTML fragment only — no <!DOCTYPE> or wrapper required. Defaults: US Letter, 1″ margins, Arial 10.5pt body text.

Getting started

Install the package. The default styleSource: "inline" path is pure JavaScript — no browser or Playwright required on Node or in the browser bundle.

Terminal
// browser or Node
npm install dom-docx

// optional — computed styles or rasterizeInPlace on Node
npm install playwright && npx playwright install chromium

Requires Node.js ≥ 20 for server-side use. For styleSource: "computed" or rasterizeInPlace on Node, install Playwright (an optional peer dependency) and its Chromium yourself, once, as shown above. The browser bundle never uses Playwright — computed styles and rasterization read from the live DOM instead.

Quick start — browser, inline styles

Use dom-docx/browser when HTML is already in your app (React, Vue, etc.). Inline styles work on a string fragment with no rendered page. Returns a Blob you can download.

Browser · inline styles
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, { styleSource: "inline" });

// trigger download (example)
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "report.docx";
a.click();
URL.revokeObjectURL(url);

Try it live on the converter page — the site installs dom-docx from npm and bundles it with Vite.

Computed styles — browser

When HTML uses <style> blocks or CSS classes instead of inline style="", set styleSource: "computed". The fragment must already be rendered in document.body — the converter reads native getComputedStyle from the live page. No Playwright, no headless Chromium.

Browser · computed styles
import { convertHtmlToDocx } from "dom-docx/browser";

const html = `
<style>
  .hero { background: #eaeaea; padding: 10px 16px; }
  .hero h1 { color: #1a1a2e; margin: 0; }
</style>
<div class="hero">
  <h1>Title</h1>
</div>
`;

// Render the same fragment in the live DOM first
document.body.innerHTML = html;

const blob = await convertHtmlToDocx(html, { styleSource: "computed" });

With a React or Vue app, convert from the component’s rendered DOM — do not pass a string that was never mounted. The converter does not inject HTML into the page for you.

Image resolver

Only data: URLs embed automatically. For http(s):, file:, or relative paths, pass an imageResolver. The library never fetches on its own — you control allowlists, auth headers, and SSRF policy. Return null to skip an image (falls back to alt text).

Browser or Node · imageResolver
const docx = await convertHtmlToDocx(html, {
  imageResolver: async (src) => {
    // YOUR policy: allowlist hosts, block private IPs, add auth…
    const url = new URL(src, window.location.href);
    if (url.hostname !== "cdn.example.com") return null;

    const res = await fetch(src);
    if (!res.ok) return null;

    return {
      data: new Uint8Array(await res.arrayBuffer()),
      type: "png", // png | jpg | gif | bmp
    };
  },
});

See the invoice example for a document with an embedded logo resolved at conversion time.

Canvas, SVG & rasterizeInPlace

Simple inline SVG (rect + text bars) converts natively. <canvas> elements, complex <svg> (paths, gradients, <use>), and chart libraries (Highcharts, Chart.js) need rasterization to PNG <img> first. Set rasterizeInPlace to snapshot those elements before conversion — use { scale: 2 } for sharper chart images (default scale is 1).

Target elements must already be rendered in the live DOM — dom-docx snapshots what is painted; it does not run chart libraries or draw to canvas for you. On Node, rasterizeInPlace uses Playwright (same prerequisite as computed styles). With a live root or Playwright page, dom-docx clones off-screen by default so your app UI is not mutated.

Image quality: pass scale: 2 (recommended for charts) to supersample PNGs at 2× pixel density while keeping layout size unchanged. Default scale is 1; max 4. Higher scale means sharper charts but larger DOCX files. rasterizeInPlace: true is shorthand for default options (scale: 1).

Option Default Description
scale 1 Supersampling factor (max 4). Recommended 2 for charts.
mutate false on live pages Replace charts in the live DOM instead of cloning off-screen.
selectors Extra CSS selectors to rasterize (e.g. [".highcharts-container"]).
Browser · rasterizeInPlace
import { convertHtmlToDocx } from "dom-docx/browser";

const root = document.querySelector(".dashboard")!;
const blob = await convertHtmlToDocx(root.innerHTML, {
  styleSource: "computed",
  root,
  rasterizeInPlace: { scale: 2 },
  // or: { selectors: [".highcharts-container"], scale: 2 },
});

On Node, rasterization runs on the same Playwright page as computed styles — either an ephemeral page spawned from an HTML string, or a page you already opened:

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

const html = `
<h1>Q3 Dashboard</h1>
<!-- canvas, complex SVG, or chart library output here -->
`;

const docx = await convertHtmlToDocx(html, {
  styleSource: "computed",
  rasterizeInPlace: { scale: 2 }, // recommended for charts
});
await writeFile("dashboard.docx", docx);
Node · live Playwright page
import { convertHtmlToDocx } from "dom-docx";

const docx = await convertHtmlToDocx(fragmentHtml, {
  styleSource: "computed",
  page,
  rootSelector: "#dashboard",
  rasterizeInPlace: { scale: 2 }, // clones off-screen unless { mutate: true }
});

See the rasterize-in-place-chart example on the showcase page for a before/after comparison with complex SVG and canvas.

Page breaks

Start a block on a new page with standard CSS — break-before / break-after (or legacy page-break-before / page-break-after). Accepted values: page, always, left, right.

CSS Effect
break-before: page New page before this block
break-after: page New page before the next block sibling

Use inline style="" on the default path, or a stylesheet with styleSource: "computed".

HTML · inline styles
<p>End of section one.</p>
<h2 style="break-before: page">Section two</h2>
<p>Section two body…</p>

<!-- or break before the next block without styling that block -->
<p style="break-after: page">Section two footer.</p>
<h2>Section three</h2>
Browser · computed stylesheet (Vue / React)
import { convertHtmlToDocx } from "dom-docx/browser";

const root = document.querySelector(".report")!;
const blob = await convertHtmlToDocx(root.innerHTML, {
  styleSource: "computed",
  root, // required when converting innerHTML from a live SPA subtree
});

Page breaks map to native Word OOXML (w:pageBreakBefore).

Node.js — inline styles

The Node entry point returns a Buffer. Best for agent-generated HTML with explicit inline styles — fast (~15–30 ms typical) with no browser dependency.

Node · inline styles
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);

Node.js — computed styles

On Node, styleSource: "computed" renders the fragment in headless Chromium via Playwright, snapshots computed styles, then converts. Slower than inline — reuse a browser instance in hot loops.

Prerequisite: Playwright is an optional peer dependency — it is not installed by npm install dom-docx. On the machine running Node, add it and its Chromium once:
npm install playwright && npx playwright install chromium

Node · computed styles
import { convertHtmlToDocx } from "dom-docx";

const html = `
<style>
  .hero { background: #eaeaea; padding: 10px 16px; }
  .hero h1 { color: #1a1a2e; margin: 0; }
</style>
<div class="hero">
  <h1>Title</h1>
</div>
`;

// launches headless Chromium via Playwright, snapshots computed styles, then converts
const docx = await convertHtmlToDocx(html, { styleSource: "computed" });

Reuse Playwright across many conversions instead of launching Chromium per call:

Node · reuse browser
import { chromium } from "playwright";
import { convertHtmlToDocx } from "dom-docx";

const browser = await chromium.launch();
try {
  for (const html of fragments) {
    const docx = await convertHtmlToDocx(html, {
      styleSource: "computed",
      browser, // reuse browser — avoids cold launch, improves throughput
    });
    // write docx…
  }
} finally {
  await browser.close();
}

Document options

Page layout, fonts, metadata, headers/footers, and locale are all optional. Omitted sides default to sensible values (e.g. margins default to 1″ per side).

Node or browser · ConvertOptions
import { convertHtmlToDocx } from "dom-docx";

const docx = await convertHtmlToDocx(html, {
  pageSize: "a4",                    // "letter" | "a4" | { width, height } in inches
  orientation: "landscape",          // "portrait" | "landscape"
  margins: { top: 0.75, bottom: 0.75 }, // inches; left/right default to 1
  defaultFont: { family: "Georgia", sizePt: 11 },
  metadata: {
    title: "Q3 Report",
    creator: "Finance",
    keywords: ["revenue", "q3"],
  },
  headerHtml: "<p style='font-size:12px;color:#666'>Confidential</p>",
  footerHtml: "<p style='font-size:12px'>© 2026 ACME</p>",
  pageNumber: true,                // centered "Page N" in footer
  coverHtml: "<h1>Quarterly Review</h1>", // cover page (before the TOC)
  tocHtml: "<ol><li><a href='#intro'>Introduction</a></li></ol>", // your TOC (see below)
  lang: "en-US",
  direction: "ltr",                  // "rtl" for right-to-left
});

Cover page

Pass coverHtml to render a cover page: it's the first content in the document — before the table of contents — followed by an automatic page break so the TOC and body start on the next page. It uses the inline style path (like the header/footer), so inline style="…" and data: images (e.g. a logo) work. Any header, footer, or page number you set is suppressed on the cover page (Word's "different first page").

Node or browser · coverHtml
import { convertHtmlToDocx } from "dom-docx";

const cover = `
<div style="text-align:center;padding-top:220px">
  <div style="font-size:12px;letter-spacing:3px;color:#64748b">ACME ANALYTICS</div>
  <h1 style="font-size:40px;color:#1a2b4a;margin:24px 0 8px">Quarterly Business Review</h1>
  <div style="font-size:18px;color:#475569">Q2 2026 Performance Report</div>
</div>`;

const docx = await convertHtmlToDocx(body, {
  coverHtml: cover,
  headerHtml: "<p>Confidential</p>", // appears on page 2+, not the cover
  pageNumber: true,                  // numbered from page 2+, not the cover
});

Headings inside the cover are not treated as body content — pair it with a tocHtml slot for a cover → contents → body document. Page numbering isn't restarted (the cover counts as page 1 internally, it's just unnumbered), so the first numbered page shows "2".

Table of contents

There's no auto-generated TOC. Instead, pass your own table-of-contents HTML as tocHtml — it's placed after the cover page (if any) and before the body, and you control the markup and styling: a numbered list, a boxed "On this page", two columns, whatever. In-page links (<a href="#id">) become clickable Word internal links that jump to the element with the matching id — dom-docx bookmarks id attributes automatically.

Node or browser · tocHtml
import { convertHtmlToDocx } from "dom-docx";

const toc = `
<div style="background:#f8fafc;border:1px solid #e2e8f0;padding:16px">
  <p style="font-weight:bold;color:#64748b">ON THIS PAGE</p>
  <ol>
    <li><a href="#intro">Introduction</a></li>
    <li><a href="#results">Results</a></li>
  </ol>
</div>
<div style="break-after:page"></div>`;   // optional: TOC on its own page

const body = `
<h2 id="intro">Introduction</h2><p>…</p>
<h2 id="results">Results</h2><p>…</p>`;

const docx = await convertHtmlToDocx(body, { tocHtml: toc });

Why a slot instead of a generated TOC? A native Word TOC is a live field: page numbers depend on final layout, which only the word processor computes, so the reader has to refresh it — with an alarming "update fields?" prompt. Worse, that generated field is hard to style (Word renders it as an undifferentiated wall of blue underlined links), and LibreOffice blanks the whole thing on "Update" unless every heading carries an explicit outline level. A caller-provided list of in-page links sidesteps all of it: you style it exactly how you want, it's complete the moment it's written, and it's correct in every viewer — Word, LibreOffice, Google Docs, PDF/preview — with no field, no prompt, and nothing to keep in sync.

Script tag (no bundler)

The npm package includes a prebuilt IIFE that exposes window.domDocx (from dist/browser/dom-docx.browser.js). Load it from a CDN or copy it from node_modules after npm install dom-docx.

HTML · script tag
<script src="https://unpkg.com/dom-docx/dist/browser/dom-docx.browser.js"></script>
<script>
  const html = "<h1 style='color:#1a1a2e'>Hello</h1>";
  const blob = await domDocx.convertHtmlToDocx(html);
  // download blob…
</script>

Try on any page (no install)

Paste this into the browser console on a live page to export a DOM section as .docx — no npm install, no bundler. It loads the prebuilt IIFE from jsDelivr (same file as the script tag section), prompts for a CSS selector, then converts with computed styles and chart rasterization.

The CDN file is an IIFE that sets window.domDocx — do not import() it as an ES module (named exports will be undefined). The sample imageResolver only embeds same-origin images — extend the allowlist for trusted CDNs if needed. Computed exports assume a light document canvas: near-white text from a dark-mode tab is remapped so it stays readable in Word.

Browser console · CDN script
(async () => {
  await new Promise((resolve, reject) => {
    const s = document.createElement("script");
    s.src = "https://cdn.jsdelivr.net/npm/dom-docx/dist/browser/dom-docx.browser.js";
    s.onload = resolve;
    s.onerror = () => reject(new Error("Failed to load dom-docx"));
    document.head.appendChild(s);
  });

  const { convertHtmlToDocx } = window.domDocx;
  const selector = prompt("CSS selector for the section to export:");
  const root = document.querySelector(selector);
  if (!root) { alert(`No match for "${selector}"`); return; }

  const blob = await convertHtmlToDocx(root.innerHTML, {
    styleSource: "computed",
    root,
    rasterizeInPlace: { scale: 2 },
    imageResolver: async (src) => {
      // only same-origin images — swap/extend this allowlist for your own trusted sources
      if (!src.startsWith(location.origin)) return null;
      const res = await fetch(src);
      if (!res.ok) return null;
      const type = res.headers.get("content-type")?.includes("png") ? "png" : "jpeg";
      return { data: new Uint8Array(await res.arrayBuffer()), type };
    },
  });

  const a = document.createElement("a");
  a.href = URL.createObjectURL(blob);
  a.download = "export.docx";
  a.click();
})();

More reference