Install
docxToOdt() is built into odf-kit — no separate package needed:
Basic usage
Import docxToOdt from odf-kit/docx and pass raw bytes. The function returns { bytes, warnings } — bytes is the complete .odt file, warnings reports any content that could not be fully converted.
import { docxToOdt } from "odf-kit/docx"; import { readFileSync, writeFileSync } from "fs"; const { bytes, warnings } = await docxToOdt(readFileSync("report.docx")); writeFileSync("report.odt", bytes); if (warnings.length > 0) { console.warn("Conversion warnings:", warnings); } // → Valid .odt file, opens in LibreOffice, Google Docs, Word
Browser usage
odf-kit/docx is pure ESM with zero new dependencies — it works in any modern browser. The conversion runs entirely client-side; the file never leaves the user's device.
import { docxToOdt } from "https://esm.sh/odf-kit@0.10.0/docx"; // From a file input const file = event.target.files[0]; const { bytes, warnings } = await docxToOdt(await file.arrayBuffer()); // Trigger download const blob = new Blob([bytes], { type: "application/vnd.oasis.opendocument.text" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = file.name.replace(/\.docx$/i, ".odt"); a.click(); URL.revokeObjectURL(url);
Options
Pass a DocxToOdtOptions object as the second argument to control the output:
const { bytes } = await docxToOdt(readFileSync("report.docx"), { // Override or fallback page format (default: read from DOCX) pageFormat: "letter", // "A4" | "letter" | "legal" | "A3" | "A5" // Override page orientation (default: read from DOCX) orientation: "portrait", // "portrait" | "landscape" // Read page size and margins from DOCX (default: true) preservePageLayout: true, // Map custom Word style names to ODT heading levels styleMap: { "Section Title": 1, "Sub Title": 2, "Article Heading": 3, }, // Override metadata (default: read from docProps/core.xml) metadata: { title: "My Report", creator: "Alice", description: "Q4 results", }, });
What is preserved
| Content | Preserved |
|---|---|
| Paragraphs and headings | ✓ Style name and outlineLvl detection |
| Bold, italic, underline, strikethrough | ✓ |
| Superscript / subscript | ✓ |
| Small caps, all caps | ✓ |
| Font size | ✓ Actual point values from w:sz |
| Text color | ✓ Hex values from w:color |
| Highlight color | ✓ |
| Font family | ✓ |
| Text alignment | ✓ left, center, right, justify |
| Paragraph spacing (before/after) | ✓ |
| Line height | ✓ |
| Indentation (left, right, first-line, hanging) | ✓ |
| Bullet lists | ✓ |
| Numbered lists (decimal, roman, alpha) | ✓ |
| Nested lists | ✓ |
| Tables | ✓ Column widths, cell backgrounds, vertical alignment |
| Merged cells (colspan and rowspan) | ✓ |
| Hyperlinks (external and internal anchors) | ✓ |
| Bookmarks | ✓ Two-pass cross-paragraph resolution |
| Images | ✓ Actual EMU dimensions, not defaulted to 10cm |
| Page layout (size, margins, orientation) | ✓ From w:sectPr |
| Headers and footers | ✓ Default type |
| Footnotes and endnotes | ✓ Appended as end section |
| Page breaks | ✓ Including mid-paragraph breaks |
| Tabs | ✓ |
| Tracked changes | ✓ Final text — insertions included, deletions skipped |
| Document metadata | ✓ From docProps/core.xml |
| Style inheritance (w:basedOn) | ✓ Full chain resolution |
| Complex fields (HYPERLINK) | ✓ w:fldChar and w:fldSimple both handled |
| Legacy VML images (w:pict) | ✓ |
| Structured document tags (w:sdt) | ✓ Unwrapped; checkboxes as ☐/☑ |
| Text boxes, charts, SmartArt | ✗ Warned and skipped |
| Math (OMML) | ✗ Out of scope |
| Macros, OLE objects | ✗ Out of scope |
Why not use mammoth.js?
mammoth.js is a well-known DOCX reader, but it has important limitations for a DOCX→ODT use case:
- CommonJS only — mammoth.js does not work in browsers via ESM, which blocks client-side use
- Intentionally drops formatting — font sizes, text colors, paragraph spacing, and line height are not preserved by design
- No page layout — page size, margins, and orientation are not read from the DOCX
- No headers or footers — these are silently ignored
- Image dimensions defaulted — all images are set to 10cm × 7.5cm regardless of the actual size in the document
- HTML intermediate step — mammoth converts to HTML first, losing structural information that ODT can represent directly
odf-kit/docx is a native DOCX→ODT converter — pure ESM, browser-safe, validated against the ECMA-376 5th edition spec.
Why not shell out to LibreOffice?
LibreOffice headless (libreoffice --headless --convert-to odt) produces high-fidelity output but comes with real costs:
- Startup time — 2–5 seconds even for small documents
- Deployment complexity — must be installed on every server, container, and CI runner
- Serverless incompatibility — doesn't run on AWS Lambda, Vercel, or Cloudflare Workers
- Size — several hundred megabytes; dominates Docker image size
- Browser impossible — cannot run client-side under any circumstances
odf-kit/docx parses DOCX files directly in JavaScript. No subprocess, no installed software, no startup penalty. Works anywhere JavaScript runs — including browsers.
Comparison
| Option | ESM / Browser | Font sizes & colors | Page layout | Images (actual size) |
|---|---|---|---|---|
| odf-kit/docx | ✓ | ✓ | ✓ | ✓ |
| mammoth.js | ✗ CommonJS | ✗ Intentional | ✗ | ✗ 10cm default |
| LibreOffice headless | ✗ Subprocess | ✓ | ✓ | ✓ |
| officeParser | ✗ CommonJS | Partial | ✗ | ✗ |
Handling warnings
The warnings array in the result reports content that could not be fully converted — unrecognised field instructions, missing image data, or mid-document section breaks. These are informational and do not cause the conversion to fail. Log them during development to understand what a specific document contains that is out of scope.
const { bytes, warnings } = await docxToOdt(input); for (const w of warnings) { console.warn("[odf-kit/docx]", w); } // Example warnings: // "Image rId5 references media/image1.wmf which is not in the ZIP" // "Unrecognized field instruction: \" MERGEFIELD CustomerName\"" // "w:altChunk (imported external content) is not supported and was skipped"
Custom style mapping
Word documents sometimes use custom style names for headings — for example "Section Title" instead of "Heading 1". Use styleMap to tell odf-kit how to map them to ODT heading levels:
const { bytes } = await docxToOdt(input, { styleMap: { "Section Title": 1, // maps to text:h level="1" "Sub Title": 2, // maps to text:h level="2" "Clause": 3, }, }); // Built-in heading detection (always applied): // "heading 1"–"heading 6" → levels 1–6 (Word canonical names) // "title" → level 1, "subtitle" → level 2 // w:outlineLvl in pPr → fallback for custom styles // styleMap overrides all of the above
Related guides and tools
- Free DOCX to ODT converter (online tool) — powered by this same library, runs in your browser
- Generate .odt files in Node.js — build documents from scratch
- ODT to HTML in JavaScript — read and convert .odt files
- Convert ODT to PDF with Typst — zero-dependency PDF generation