Create .odt documents from scratch, fill existing templates with data, convert DOCX to ODT, HTML to ODT, Markdown to ODT, Lexical JSON to ODT, read .odt files, or export to Typst for PDF — all in pure JavaScript. Works in Node.js and the browser. No LibreOffice required.
Any application — server-side or in the browser — that needs to create formatted documents as files. Not PDFs, not proprietary formats — the open standard that LibreOffice, governments, and schools actually use.
Many governments mandate OpenDocument Format (ODF) per ISO/IEC 26300. Generate permits, notices, tax documents, and citizen correspondence in the required open standard format.
Add "Export as .odt" to your web application. Generate invoices and reports server-side, or directly in the browser — user data never leaves the page. Files open in LibreOffice or Word.
Design templates in LibreOffice with {placeholders}, then fill them with data from your database. Generate grant reports, donor letters, transcripts, and certificates at scale.
Fill contract templates with client data, loop over line items, conditionally include clauses. Produce vendor-independent files in the ISO document standard.
Generate changelogs, release notes, test reports, and documentation as .odt files in your build pipeline. No desktop software required — odf-kit runs in Node.js, browsers, Deno, and Bun.
Build tools for food banks, legal aid, refugee services, and community groups. Generate case files, intake forms, and service records in a format that works with free office software.
Build ODT from scratch, fill templates, convert DOCX→ODT, HTML→ODT, Markdown→ODT, TipTap JSON→ODT, build ODS spreadsheets, read ODT and ODS files, convert XLSX→ODS, convert ODT→Markdown, or export to Typst for PDF — all in pure JavaScript.
import { OdtDocument } from "odf-kit"; const doc = new OdtDocument(); doc.setMetadata({ title: "Quarterly Report" }); doc.setFooter("Page ###"); doc.addHeading("Q4 Results", 1); doc.addParagraph("Revenue exceeded expectations across all regions."); doc.addTable([ ["Region", "Revenue", "Growth"], ["North", "$2.1M", "+12%"], ["South", "$1.8M", "+8%"], ], { border: "0.5pt solid #000" }); doc.addParagraph((p) => { p.addText("Status: "); p.addText("Approved", { bold: true, color: "green" }); }); const bytes = await doc.save(); // → Uint8Array — valid .odt, opens in LibreOffice
import { fillTemplate } from "odf-kit"; import { readFileSync, writeFileSync } from "fs"; // Load a .odt template created in LibreOffice const template = readFileSync("invoice-template.odt"); const result = fillTemplate(template, { customer: "Acme Corp", date: "2026-04-12", items: [ { product: "Widget", qty: 5, price: "$125" }, { product: "Gadget", qty: 3, price: "$120" }, ], showNotes: true, notes: "Net 30", company: { name: "Acme Corp", address: "123 Main St" }, }); writeFileSync("invoice.odt", result); // {placeholders}, {#loops}, conditionals, dot.notation
import { htmlToOdt } from "odf-kit"; import { writeFileSync } from "fs"; const html = ` <h1>Meeting Notes</h1> <p>Attendees: <strong>Alice</strong>, Bob, Carol</p> <ul> <li>Project status — on track</li> <li>Budget review — approved</li> </ul> <table> <tr><th>Action</th><th>Owner</th></tr> <tr><td>Send report</td><td>Alice</td></tr> </table> `; const bytes = await htmlToOdt(html, { pageFormat: "A4" }); writeFileSync("notes.odt", bytes); // → Headings, bold, lists, tables, inline CSS all preserved
import { markdownToOdt } from "odf-kit"; import { writeFileSync } from "fs"; const markdown = ` # Quarterly Report Revenue **exceeded expectations** across all regions. ## Action Items - Send report by Friday - Review budget on Monday | Region | Revenue | |--------|---------| | North | $2.1M | | South | $1.8M | `; const bytes = await markdownToOdt(markdown, { pageFormat: "A4" }); writeFileSync("report.odt", bytes); // → Full CommonMark — headings, bold, lists, tables
import { tiptapToOdt } from "odf-kit"; // editor.getJSON() returns TipTap/ProseMirror JSONContent const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" }); // With pre-fetched images (IPFS, S3, etc.) const images = { [imageUrl]: await fetchImageBytes(imageUrl) }; const bytes2 = await tiptapToOdt(editor.getJSON(), { images }); // Custom handler for app-specific TipTap extensions const bytes3 = await tiptapToOdt(editor.getJSON(), { unknownNodeHandler: (node, doc) => { if (node.type === "callout") doc.addParagraph(`⚠️ ${node.text}`); }, }); // → No @tiptap/core dependency — walks JSON directly
import { OdsDocument } from "odf-kit"; import { writeFileSync } from "fs"; const doc = new OdsDocument(); const sheet = doc.addSheet("Sales"); sheet.addRow( ["Month", "Revenue", "Growth"], { bold: true, backgroundColor: "#DDDDDD" } ); sheet.addRow(["January", 12500, 0.08]); sheet.addRow(["February", 14200, 0.136]); sheet.addRow(["Total", { value: "=SUM(B2:B3)", type: "formula" }]); sheet.setColumnWidth(0, "4cm"); sheet.freezeRows(1); const bytes = await doc.save(); writeFileSync("sales.ods", bytes); // → Valid .ods — opens in LibreOffice Calc
import { readOdt, odtToHtml } from "odf-kit/reader"; import { readFileSync } from "node:fs"; const bytes = new Uint8Array(readFileSync("document.odt")); // Convert to HTML in one call const html = odtToHtml(bytes, { fragment: true }); // Or parse to a structured document model const doc = readOdt(bytes); console.log(doc.metadata.title); for (const node of doc.body) { if (node.kind === "heading") console.log(`h${node.level}:`, node.spans[0].text); } // → Paragraphs, headings, tables, lists, formatting
import { readOds, odsToHtml } from "odf-kit/ods-reader"; import { readFileSync } from "node:fs"; const bytes = readFileSync("data.ods"); // Parse to a structured model — typed JavaScript values const model = readOds(bytes); for (const row of model.sheets[0].rows) { for (const cell of row.cells) { console.log(cell.type, cell.value); // string | float | boolean | date — never display strings } } // Or convert directly to an HTML table const html = odsToHtml(bytes); // → <table> with merged cells, inline styles
import { xlsxToOds } from "odf-kit/xlsx"; import { readFileSync, writeFileSync } from "fs"; // Convert .xlsx to .ods — zero new dependencies const xlsx = readFileSync("report.xlsx"); const ods = await xlsxToOds(xlsx); writeFileSync("report.ods", ods); // Preserves strings, numbers, booleans, dates, // formulas, merged cells, freeze panes, multi-sheet // No SheetJS — own parser built on fflate // Also works in the browser const ods2 = await xlsxToOds(await file.arrayBuffer());
import { docxToOdt } from "odf-kit/docx"; import { readFileSync, writeFileSync } from "fs"; // Simple conversion const { bytes, warnings } = await docxToOdt(readFileSync("report.docx")); writeFileSync("report.odt", bytes); if (warnings.length) console.warn(warnings); // With options — page format, custom style mapping const { bytes: bytes2 } = await docxToOdt(readFileSync("report.docx"), { pageFormat: "letter", styleMap: { "Section Title": 1 }, }); // Also works in the browser — pure ESM, zero new dependencies const { bytes: odt } = await docxToOdt(await file.arrayBuffer());
import { odtToMarkdown } from "odf-kit/markdown"; import { readFileSync, writeFileSync } from "fs"; // GFM (default) — pipe tables, ~~strikethrough~~ const md = odtToMarkdown(readFileSync("document.odt")); writeFileSync("document.md", md); // CommonMark flavor const md2 = odtToMarkdown(readFileSync("document.odt"), { flavor: "commonmark", }); // Also works in the browser — pure ESM, zero dependencies const md3 = odtToMarkdown(new Uint8Array(await file.arrayBuffer()));
import { odtToTypst, modelToTypst } from "odf-kit/typst"; import { readOdt } from "odf-kit/reader"; import { readFileSync, writeFileSync } from "node:fs"; import { execSync } from "node:child_process"; const bytes = new Uint8Array(readFileSync("document.odt")); const typ = odtToTypst(bytes); writeFileSync("document.typ", typ); execSync("typst compile document.typ document.pdf"); // Parse once, emit to multiple formats const model = readOdt(bytes); const typst = modelToTypst(model); // odf-kit/typst is zero-dependency and browser-safe
import { lexicalToOdt } from "odf-kit/lexical"; // editor.getEditorState().toJSON() returns SerializedEditorState const bytes = await lexicalToOdt(editor.getEditorState().toJSON(), { pageFormat: "A4", }); // With image resolution for remote URLs const bytes2 = await lexicalToOdt(editor.getEditorState().toJSON(), { pageFormat: "A4", fetchImage: async (src) => { const res = await fetch(src); return new Uint8Array(await res.arrayBuffer()); }, }); // → No Lexical dependency — walks SerializedEditorState directly
Built from the ground up for the ODF 1.2+ specification. Every feature validated against LibreOffice 24.2. Works in Node.js and modern browsers.
Fill existing .odt templates with {placeholders}. Loops for repeating content, conditionals for optional sections, dot notation for nested data.
Bold, italic, underline, strikethrough, superscript, subscript. Custom fonts, sizes, colors, and highlights.
Column widths, cell borders, backgrounds, merged cells (colspan & rowspan). Formatted text inside cells.
Page size, margins, orientation. Headers and footers with page numbers. Page breaks.
Bullet and numbered lists with up to 6 levels of nesting. Formatted items via builder callback.
Embedded PNG, JPEG, GIF, SVG, WebP, BMP, TIFF. Standalone or inline. Sized and anchored.
External hyperlinks, internal bookmark links. Formatted link text. Named anchor points.
Parse existing .odt files into a structured document model. Convert to HTML with
odtToHtml(). Paragraphs, headings, tables, lists, formatting, hyperlinks.
Pure JavaScript, no LibreOffice needed.
Convert any .odt file to Typst markup with odtToTypst(), then compile to
PDF with the Typst CLI. The only JavaScript library with a built-in ODT-to-Typst
emitter. Zero new dependencies.
Two runtime dependencies: fflate (ZIP) and marked (Markdown parsing). Zero transitive dependencies from either. Lightweight and audit-friendly for enterprise and government projects.
Same API everywhere. Generate documents server-side in Node.js or client-side in the browser. Also works in Deno, Bun, and Cloudflare Workers.
The JS/TS ecosystem had no maintained, full-featured library for OpenDocument Format generation. odf-kit fills that gap.
Build ODT from scratch, fill templates, convert DOCX→ODT, HTML→ODT, Markdown→ODT, TipTap JSON→ODT, Lexical JSON→ODT, build ODS spreadsheets, read ODT and ODS files, convert XLSX→ODS, convert ODT→Markdown, or export to Typst for PDF. Same API in Node.js and browsers — no LibreOffice required.
Two runtime dependencies: fflate (ZIP) and marked (Markdown parsing) — zero transitive dependencies from either. Lightweight and audit-friendly for enterprise and government projects. Every sub-export maintains this guarantee.
Generates proper ZIP-packaged .odt files per the ODF international standard. Opens in LibreOffice, Google Docs, and any compliant application.
ODF is the ISO standard used by the EU, governments, schools, and nonprofits worldwide. odf-kit is Apache 2.0 — use it anywhere, commercial or not.
Simple things are one-liners. Complex output uses builder callbacks. Both
bold: true and fontWeight: "bold" work. Flexible and
forgiving.
The only prior JS library (simple-odf) was abandoned in 2021. It had no table support and only generated flat XML. odf-kit produces proper .odt files with full formatting.
odf-kit is the ODF replacement for LibreOffice headless, the SheetJS alternative for .ods files, and the free open source alternative to docxtemplater for ODF templates.
| Feature | odf-kit | simple-odf |
|---|---|---|
| Maintained | ✓ Active (2026) | ✗ Abandoned (2021) |
| Output format | ✓ Proper .odt ZIP packages | ✗ Flat XML only (.fodt) |
| Template engine | ✓ Loops, conditionals, dot notation | ✗ Not supported |
| Tables | ✓ Full support with merging | ✗ Not supported |
| Images | ✓ Embedded in ZIP | ✗ Not supported |
| Lists | ✓ Bullet & numbered, nested | Partial |
| Page layout | ✓ Margins, headers, footers | ✗ Not supported |
| Hyperlinks & bookmarks | ✓ External and internal | Partial |
| Convert DOCX → ODT | ✓ Native, browser-safe, zero extra deps | ✗ Not supported |
| Convert XLSX → ODS (SheetJS alternative for ODF) | ✓ Zero deps, own parser | ✗ Not supported |
| Read & convert to HTML | ✓ Full reader with document model | ✗ Not supported |
| Convert ODT → Markdown | ✓ GFM and CommonMark | ✗ Not supported |
| Export to Typst / PDF (LibreOffice headless alternative) | ✓ Built-in Typst emitter | ✗ Not supported |
| TypeScript | ✓ Written in TypeScript | ✓ Written in TypeScript |
| Browser support | ✓ Node.js and browsers | ✗ Node.js only |
Install odf-kit with npm install odf-kit, then use the
OdtDocument class to build documents from scratch, or use
fillTemplate() to fill existing .odt templates with data. Works in Node.js
and browsers. Call save() to get a Uint8Array, then write it
to disk or trigger a browser download. See the
step-by-step guide.
Yes. Create a template in LibreOffice with {placeholders} in the text, then use
fillTemplate(templateBytes, data). It supports simple replacement, loops
for repeating content ({#items}...{/items}), conditionals for optional sections, and dot
notation for nested objects ({company.name}). The engine automatically handles
LibreOffice's XML fragmentation of placeholder text. See the
template guide for a full
walkthrough.
Yes. Import odtToTypst() from odf-kit/typst to convert any
.odt file to Typst markup, then compile to PDF with the
Typst CLI (typst compile document.typ document.pdf). odf-kit is the only JavaScript library with a built-in ODT-to-Typst emitter. The
emitter is zero-dependency and works in any environment — Node.js, browser, Deno, or
Nextcloud Flow. See the ODT to PDF guide for
a full walkthrough.
Yes. simple-odf was abandoned in 2021 and only produced flat XML files (.fodt), not proper .odt ZIP packages. It had no support for tables, images, page layout, templates, or bookmarks. odf-kit is a modern, actively maintained alternative that generates compliant .odt files with comprehensive formatting support. See the migration guide for a side-by-side comparison.
odf-kit generates ODF 1.2+ documents per ISO/IEC 26300, the international standard mandated by the European Union, many national governments, and public sector organizations. The output opens in LibreOffice and any ODF-compliant application. See the government compliance guide for a full list of mandates and implementation details.
Yes. odf-kit works both server-side in Node.js and client-side in the browser. You can generate .odt files on your backend and serve them as downloads, or generate them entirely in the browser — user data never leaves the page. The files open natively in LibreOffice Writer, and also work in Google Docs, Microsoft Word, and other applications that support ODF. See the browser generation guide for a complete walkthrough.
Yes. Import readOdt() or odtToHtml() from
odf-kit/reader to parse existing .odt files. readOdt() returns
a structured document model with metadata, paragraphs, headings, tables, lists, and text
formatting. odtToHtml() converts directly to HTML. Works in Node.js and
browsers with zero dependencies — no LibreOffice required. See the
ODT to HTML guide for a full
walkthrough, or the
ODT ecosystem guide for a comparison
of all options.
.odt is the OpenDocument Format — an ISO international standard (ISO/IEC 26300) that's
vendor-independent and open. .docx is Microsoft's Office Open XML format. ODF is
required by many governments and organizations that need vendor independence. odf-kit
generates .odt files, and can also convert .docx files to .odt using
docxToOdt() from odf-kit/docx — pure ESM, browser-safe, zero
extra dependencies.
We have detailed guides for common tasks:
Install odf-kit and create your first .odt file in under a minute. Works in Node.js and browsers. Build from scratch, fill templates, convert DOCX to ODT, Lexical JSON to ODT, read existing .odt files, convert to Markdown, or export to Typst for PDF.