v0.12.3 · Apache 2.0 · TypeScript + JavaScript

Generate, fill, read, and convert
OpenDocument files with odf-kit

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.

$ npm install odf-kit

Who generates ODF documents from code?

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.

Government & Public Sector

Compliance document generation

Many governments mandate OpenDocument Format (ODF) per ISO/IEC 26300. Generate permits, notices, tax documents, and citizen correspondence in the required open standard format.

SaaS & Web Apps

Report and invoice export

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.

Nonprofits & Education

Template-driven mail merge

Design templates in LibreOffice with {placeholders}, then fill them with data from your database. Generate grant reports, donor letters, transcripts, and certificates at scale.

Legal & HR

Contract and document assembly

Fill contract templates with client data, loop over line items, conditionally include clauses. Produce vendor-independent files in the ISO document standard.

DevOps & CI/CD

Build pipeline document generation

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.

Civic Tech

Community organization tools

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.

Thirteen ways to work with ODF files

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

Everything you need to work with .odt files

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.

📋

Template Engine

Fill existing .odt templates with {placeholders}. Loops for repeating content, conditionals for optional sections, dot notation for nested data.

✏️

Text Formatting

Bold, italic, underline, strikethrough, superscript, subscript. Custom fonts, sizes, colors, and highlights.

📊

Tables

Column widths, cell borders, backgrounds, merged cells (colspan & rowspan). Formatted text inside cells.

📄

Page Layout

Page size, margins, orientation. Headers and footers with page numbers. Page breaks.

📝

Lists

Bullet and numbered lists with up to 6 levels of nesting. Formatted items via builder callback.

🖼️

Images

Embedded PNG, JPEG, GIF, SVG, WebP, BMP, TIFF. Standalone or inline. Sized and anchored.

🔗

Hyperlinks & Bookmarks

External hyperlinks, internal bookmark links. Formatted link text. Named anchor points.

📖

Read & Convert to HTML

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.

📑

Export to Typst for PDF

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.

Minimal Dependencies

Two runtime dependencies: fflate (ZIP) and marked (Markdown parsing). Zero transitive dependencies from either. Lightweight and audit-friendly for enterprise and government projects.

🌐

Node.js & Browser

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 JavaScript ODF library that was missing

The JS/TS ecosystem had no maintained, full-featured library for OpenDocument Format generation. odf-kit fills that gap.

Thirteen modes, one library, every platform

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.

Minimal dependencies

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.

ODF 1.2+ and ISO/IEC 26300

Generates proper ZIP-packaged .odt files per the ODF international standard. Opens in LibreOffice, Google Docs, and any compliant application.

Open standards, open source

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.

Developer-friendly API

Simple things are one-liners. Complex output uses builder callbacks. Both bold: true and fontWeight: "bold" work. Flexible and forgiving.

Replaces simple-odf

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 vs. the alternatives

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
1113
Tests passing
0
Transitive dependencies
13
Document modes
0
External XML parsers

Common questions

How do I generate an ODF document in JavaScript or Node.js?

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.

Can I fill an existing .odt template with data?

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.

Can I convert an ODT file to PDF without LibreOffice?

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.

Is odf-kit a replacement for simple-odf?

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.

Does odf-kit work for government ODF compliance requirements?

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.

Can I use odf-kit to create LibreOffice documents from my web application?

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.

Can I read or convert an existing .odt file to HTML?

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.

What's the difference between .odt and .docx?

.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.

Where can I find step-by-step tutorials?

We have detailed guides for common tasks:

Start working with ODF documents

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.

$ npm install odf-kit