Generate, fill, read, and convert OpenDocument Format files (.odt, .ods) in TypeScript and JavaScript. Convert HTML, Markdown, TipTap JSON, Lexical JSON, and DOCX to ODT. Works in Node.js and browsers. No LibreOffice dependency — pure spec-compliant ODF.
npm install odf-kit// 1. Build an ODT document from scratch
import { OdtDocument } from "odf-kit";
const doc = new OdtDocument();
doc.addHeading("Quarterly Report", 1);
doc.addParagraph("Revenue exceeded expectations.");
doc.addTable([
["Division", "Q4 Revenue", "Growth"],
["North", "$2.1M", "+12%"],
["South", "$1.8M", "+8%"],
]);
const bytes = await doc.save();// 2. Convert HTML to ODT
import { htmlToOdt } from "odf-kit";
const html = `
<h1>Meeting Notes</h1>
<p>Attendees: <strong>Alice</strong>, Bob, Carol</p>
<ul>
<li>Project status</li>
<li>Budget review</li>
</ul>
`;
const bytes = await htmlToOdt(html, { pageFormat: "A4" });// 3. Convert Markdown to ODT
import { markdownToOdt } from "odf-kit";
const markdown = `
# Meeting Notes
Attendees: **Alice**, Bob, Carol
## Action Items
- Send report by Friday
- Review budget on Monday
`;
const bytes = await markdownToOdt(markdown, { pageFormat: "A4" });// 4. Convert TipTap/ProseMirror JSON to ODT
import { tiptapToOdt } from "odf-kit";
// editor.getJSON() returns TipTap JSONContent
const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" });
// With pre-fetched images (e.g. from IPFS or S3)
const images = { [imageUrl]: await fetchImageBytes(imageUrl) };
const bytes2 = await tiptapToOdt(editor.getJSON(), { images });
// With custom node handler for app-specific extensions
const bytes3 = await tiptapToOdt(editor.getJSON(), {
unknownNodeHandler: (node, doc) => {
if (node.type === "callout") doc.addParagraph(`⚠️ ${extractText(node)}`);
},
});// 5. Build an ODS spreadsheet from scratch
import { OdsDocument } from "odf-kit";
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.setColumnWidth(1, "4cm");
const bytes = await doc.save();// 6. Fill an existing .odt template with data
import { fillTemplate } from "odf-kit";
const template = readFileSync("invoice-template.odt");
const result = fillTemplate(template, {
customer: "Acme Corp",
date: "2026-03-19",
items: [
{ product: "Widget", qty: 5, price: "$125" },
{ product: "Gadget", qty: 3, price: "$120" },
],
showNotes: true,
notes: "Net 30",
});
writeFileSync("invoice.odt", result);// 7. Read an existing .odt file
import { readOdt, odtToHtml } from "odf-kit/reader";
const bytes = readFileSync("report.odt");
const model = readOdt(bytes); // structured document model
const html = odtToHtml(bytes); // styled HTML string// 8. Read an existing .ods spreadsheet
import { readOds, odsToHtml } from "odf-kit/ods-reader";
const bytes = readFileSync("data.ods");
const model = readOds(bytes); // structured model — typed values
const html = odsToHtml(bytes); // HTML table string// 9. Convert .xlsx to .ods — no external dependencies
import { xlsxToOds } from "odf-kit/xlsx"
const bytes = await xlsxToOds(readFileSync("report.xlsx"))
writeFileSync("report.ods", bytes)// 10. Convert .odt to Typst for PDF generation
import { odtToTypst } from "odf-kit/typst";
import { execSync } from "child_process";
const typst = odtToTypst(readFileSync("letter.odt"));
writeFileSync("letter.typ", typst);
execSync("typst compile letter.typ letter.pdf");// 11. Convert .docx to .odt — pure ESM, zero new dependencies, browser-safe
import { docxToOdt } from "odf-kit/docx";
const { bytes, warnings } = await docxToOdt(readFileSync("report.docx"));
writeFileSync("report.odt", bytes);
if (warnings.length > 0) console.warn(warnings);
// With options
const { bytes: bytes2 } = await docxToOdt(readFileSync("report.docx"), {
pageFormat: "letter",
styleMap: { "Section Title": 1 }, // map custom Word style → heading level
});// 12. Convert .odt to Markdown
import { odtToMarkdown } from "odf-kit/markdown";
const md = odtToMarkdown(readFileSync("document.odt"));
writeFileSync("document.md", md);
// CommonMark flavor (no pipe tables)
const mdCompat = odtToMarkdown(readFileSync("document.odt"), { flavor: "commonmark" });// 13. Convert Lexical editor state to ODT
import { lexicalToOdt } from "odf-kit/lexical";
// editor.getEditorState().toJSON() returns SerializedEditorState
const bytes = await lexicalToOdt(editor.getEditorState().toJSON(), { pageFormat: "A4" });
// With image resolution (e.g. for Proton Docs integration)
const bytes2 = await lexicalToOdt(editor.getEditorState().toJSON(), {
pageFormat: "A4",
fetchImage: async (src) => {
const response = await fetch(src);
return new Uint8Array(await response.arrayBuffer());
},
});npm install odf-kitNode.js 22+ required. ESM only. Sub-exports:
import { OdtDocument, OdsDocument, htmlToOdt, markdownToOdt, tiptapToOdt, fillTemplate } from "odf-kit";
import { readOdt, odtToHtml } from "odf-kit/odt-reader";
import { readOds, odsToHtml } from "odf-kit/ods-reader";
import { odtToTypst, modelToTypst } from "odf-kit/typst";
import { docxToOdt } from "odf-kit/docx";
import { odtToMarkdown, modelToMarkdown } from "odf-kit/markdown";
import { lexicalToOdt } from "odf-kit/lexical";Works in Node.js, browsers, Deno, Bun, and Cloudflare Workers. Runtime dependencies: fflate for ZIP, marked for Markdown parsing.
odf-kit generates and reads documents entirely client-side. No server required.
import { OdtDocument } from "odf-kit";
const doc = new OdtDocument();
doc.addHeading("Generated in the Browser", 1);
doc.addParagraph("Created without any server.");
const bytes = await doc.save();
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 = "document.odt";
a.click();
URL.revokeObjectURL(url);Template filling and reading work the same way — pass Uint8Array bytes from a <input type="file"> or fetch().
doc.addHeading("Chapter 1", 1);
doc.addParagraph((p) => {
p.addText("This is ");
p.addText("bold", { bold: true });
p.addText(", ");
p.addText("italic", { italic: true });
p.addText(", and ");
p.addText("red", { color: "red", fontSize: 16 });
p.addText(".");
});
// Scientific notation
doc.addParagraph((p) => {
p.addText("H");
p.addText("2", { subscript: true });
p.addText("O is ");
p.addText("essential", { underline: true, highlightColor: "yellow" });
});// Simple
doc.addTable([
["Name", "Age", "City"],
["Alice", "30", "Portland"],
["Bob", "25", "Seattle"],
]);
// With column widths and borders
doc.addTable([
["Product", "Price"],
["Widget", "$9.99"],
], { columnWidths: ["8cm", "4cm"], border: "0.5pt solid #000000" });
// Full control — builder callback
doc.addTable((t) => {
t.addRow((r) => {
r.addCell("Name", { bold: true, backgroundColor: "#DDDDDD" });
r.addCell("Status", { bold: true, backgroundColor: "#DDDDDD" });
});
t.addRow((r) => {
r.addCell((c) => { c.addText("Project Alpha", { bold: true }); });
r.addCell("Complete", { color: "green" });
});
}, { columnWidths: ["8cm", "4cm"] });doc.setPageLayout({
orientation: "landscape",
marginTop: "1.5cm",
marginBottom: "1.5cm",
});
doc.setHeader((h) => {
h.addText("Confidential", { bold: true, color: "gray" });
h.addText(" — Page ");
h.addPageNumber();
});
doc.setFooter("© 2026 Acme Corp — Page ###"); // ### = page number
doc.addPageBreak();doc.addList(["Apples", "Bananas", "Cherries"]);
doc.addList(["First", "Second", "Third"], { type: "numbered" });
// Nested with formatting
doc.addList((l) => {
l.addItem((p) => {
p.addText("Important: ", { bold: true });
p.addText("read the docs");
});
l.addItem("Main topic");
l.addNested((sub) => {
sub.addItem("Subtopic A");
sub.addItem("Subtopic B");
});
});import { readFile } from "fs/promises";
const logo = await readFile("logo.png");
doc.addImage(logo, { width: "10cm", height: "6cm", mimeType: "image/png" });
// Inline image inside a paragraph
doc.addParagraph((p) => {
p.addText("Logo: ");
p.addImage(logo, { width: "2cm", height: "1cm", mimeType: "image/png" });
});doc.addParagraph((p) => {
p.addBookmark("introduction");
p.addText("Welcome to the guide.");
});
doc.addParagraph((p) => {
p.addLink("our website", "https://example.com", { bold: true });
p.addText(" or go back to the ");
p.addLink("introduction", "#introduction");
});doc.addParagraph((p) => {
p.addText("Item"); p.addTab();
p.addText("Qty"); p.addTab();
p.addText("$100.00");
}, {
tabStops: [
{ position: "6cm" },
{ position: "12cm", type: "right" },
],
});const bytes = await new OdtDocument()
.setMetadata({ title: "Report" })
.setPageLayout({ orientation: "landscape" })
.setHeader("Confidential")
.setFooter("Page ###")
.addHeading("Summary", 1)
.addParagraph("All systems operational.")
.addTable([["System", "Status"], ["API", "OK"], ["DB", "OK"]])
.save();OdsDocument generates .ods spreadsheet files with multiple sheets, typed cells, formatting, and formulas.
Values are auto-typed from their JavaScript type. Use an explicit OdsCellObject when you need formulas or per-cell overrides.
import { OdsDocument } from "odf-kit";
const doc = new OdsDocument();
const sheet = doc.addSheet("Data");
sheet.addRow([
"Text", // string
42, // float
new Date("2026-01-15"), // date
true, // boolean
null, // empty cell
{ value: "=SUM(B1:B10)", type: "formula" }, // formula — explicit required
]);// Bold header row with background
sheet.addRow(["Month", "Revenue", "Notes"], {
bold: true,
backgroundColor: "#DDDDDD",
align: "center",
});
// Mixed: row default + per-cell override
sheet.addRow([
"January",
{ value: 12500, type: "float", color: "#006600" },
"On track",
], { italic: true });doc.setDateFormat("DD/MM/YYYY"); // "YYYY-MM-DD" | "DD/MM/YYYY" | "MM/DD/YYYY"
sheet.addRow([{ value: new Date("2026-12-25"), type: "date", dateFormat: "MM/DD/YYYY" }]);sheet.setColumnWidth(0, "4cm");
sheet.setColumnWidth(1, "8cm");
sheet.setRowHeight(0, "1.5cm");const doc = new OdsDocument();
const q1 = doc.addSheet("Q1").setTabColor("#4CAF50");
const q2 = doc.addSheet("Q2").setTabColor("#2196F3");
q1.addRow(["Month", "Revenue"], { bold: true });
q1.addRow(["January", 12500]);
const q2sheet = doc.addSheet("Summary");
q2sheet.addRow(["Total", 27700]);
const bytes = await doc.save();sheet.addRow([{ value: 9999, type: "float", numberFormat: "integer" }]); // 9,999
sheet.addRow([{ value: 1234.567, type: "float", numberFormat: "decimal:2" }]); // 1,234.57
sheet.addRow([{ value: 0.1234, type: "percentage", numberFormat: "percentage" }]); // 12.34%
sheet.addRow([{ value: 0.075, type: "percentage", numberFormat: "percentage:1" }]);// 7.5%
sheet.addRow([{ value: 1234.56, type: "currency", numberFormat: "currency:EUR" }]);// €1,234.56
sheet.addRow([{ value: 99.99, type: "currency", numberFormat: "currency:USD:0" }]);// $100
// Row-level number format — applies to all cells in the row
sheet.addRow([1000, 2000, 3000], { numberFormat: "integer" });// Span across 3 columns
sheet.addRow([{ value: "Q1 Sales Report", type: "string", colSpan: 3, bold: true }]);
sheet.addRow(["Region", "Units", "Revenue"]);
// Span across 2 rows
sheet.addRow([{ value: "North", type: "string", rowSpan: 2 }, "Jan", 12500]);
sheet.addRow(["Feb", 14200]); // "North" continues from above
// Combined colSpan + rowSpan
sheet.addRow([{ value: "Big Cell", type: "string", colSpan: 2, rowSpan: 2 }, "C"]);// Freeze the header row
sheet.addRow(["Name", "Amount", "Date"], { bold: true });
sheet.freezeRows(1);
// Freeze first column
sheet.freezeColumns(1);
// Both
sheet.freezeRows(1).freezeColumns(1);sheet.addRow([{
value: "odf-kit on GitHub",
type: "string",
href: "https://github.com/GitHubNewbie0/odf-kit",
}]);doc.addSheet("Q1").setTabColor("#4CAF50"); // green
doc.addSheet("Q2").setTabColor("#2196F3"); // blue
doc.addSheet("Q3").setTabColor("#F44336"); // redhtmlToOdt() converts an HTML string to a .odt file. The primary use case is Nextcloud Text ODT export and any web-based editor that stores content as HTML.
import { htmlToOdt } from "odf-kit";
const bytes = await htmlToOdt(html); // A4 default
const bytes = await htmlToOdt(html, { pageFormat: "letter" }); // US letter| Format | Dimensions | Default margins | Typical use |
|---|---|---|---|
"A4" |
21 × 29.7 cm | 2.5 cm | Europe, ISO standard (default) |
"letter" |
21.59 × 27.94 cm | 2.54 cm | USA, Canada |
"legal" |
21.59 × 35.56 cm | 2.54 cm | USA legal |
"A3" |
29.7 × 42 cm | 2.5 cm | Large format |
"A5" |
14.8 × 21 cm | 2 cm | Small booklets |
Block: <h1>–<h6>, <p>, <ul>, <ol>, <li> (nested), <table> / <tr> / <td> / <th>, <blockquote>, <pre>, <hr>, <figure> / <figcaption>, <div> / <section> (transparent).
Inline: <strong>, <em>, <u>, <s>, <sup>, <sub>, <a href>, <code>, <mark>, <span style="">, <br>.
markdownToOdt() converts any CommonMark Markdown string to ODT. Accepts the same options as htmlToOdt().
import { markdownToOdt } from "odf-kit";
const bytes = await markdownToOdt(markdownString, { pageFormat: "A4" });
const bytes = await markdownToOdt(markdownString, {
pageFormat: "letter",
metadata: { title: "My Document", creator: "Alice" },
});Supports headings, paragraphs, bold, italic, lists (nested), tables, links, blockquotes, code blocks, and horizontal rules.
tiptapToOdt() converts TipTap/ProseMirror JSONContent directly to ODT. No dependency on @tiptap/core — walks the JSON tree as a plain object. This is the most direct integration path for any TipTap-based editor (dDocs, Outline, Novel, BlockNote, etc.).
Conversion happens entirely in your environment. No document content is sent to external services — unlike cloud-based ODT conversion APIs. Suitable for sensitive documents, air-gapped environments, and applications with GDPR or data sovereignty requirements.
import { tiptapToOdt } from "odf-kit";
// Basic usage
const bytes = await tiptapToOdt(editor.getJSON(), { pageFormat: "A4" });
// With pre-fetched images
const images = {
"https://example.com/photo.jpg": jpegBytes,
"ipfs://Qm...": ipfsImageBytes,
};
const bytes = await tiptapToOdt(editor.getJSON(), { images });
// With custom node handler for app-specific extensions
const bytes = await tiptapToOdt(editor.getJSON(), {
unknownNodeHandler: (node, doc) => {
if (node.type === "callout") {
doc.addParagraph(`⚠️ ${node.content?.[0]?.content?.[0]?.text ?? ""}`)
}
},
});Block: doc, paragraph, heading (1–6), bulletList, orderedList, listItem (nested), blockquote, codeBlock, horizontalRule, hardBreak, image, table, tableRow, tableCell, tableHeader.
Marks: bold, italic, underline, strike, code, link, textStyle (color, fontSize, fontFamily), highlight, superscript, subscript.
Images: Data URIs are decoded and embedded directly. Other URLs are looked up in the images option. Unknown URLs emit a [Image: alt] placeholder paragraph.
Unknown nodes: Silently skipped by default. Provide unknownNodeHandler to handle custom extensions.
Create a .odt template in LibreOffice with {placeholders}, then fill it programmatically.
Dear {name},
Your order #{orderNumber} has shipped to {address}.
Company: {company.name}
City: {company.address.city}
{#items}
Product: {product} — Qty: {qty} — Price: {price}
{/items}
{#showDiscount}
You qualify for a {percent}% discount!
{/showDiscount}
Falsy values (false, null, undefined, 0, "", []) remove the block. Truthy values include it.
odf-kit/reader parses .odt files into a structured model and renders to HTML.
import { readOdt, odtToHtml } from "odf-kit/odt-reader";
const bytes = readFileSync("report.odt");
const model = readOdt(bytes);
const html = odtToHtml(bytes);
// Tracked changes
const final = odtToHtml(bytes, {}, { trackedChanges: "final" });
const original = odtToHtml(bytes, {}, { trackedChanges: "original" });
const marked = odtToHtml(bytes, {}, { trackedChanges: "changes" });odf-kit/ods-reader parses .ods files into a structured model and renders to HTML.
import { readOds, odsToHtml } from "odf-kit/ods-reader";
import { readFileSync } from "fs";
const bytes = readFileSync("data.ods");
// Structured model — typed JavaScript values
const model = readOds(bytes);
for (const sheet of model.sheets) {
console.log(sheet.name);
for (const row of sheet.rows) {
for (const cell of row.cells) {
console.log(cell.colIndex, cell.type, cell.value);
// e.g. 0 "float" 1234.56
// e.g. 1 "string" "Hello"
// e.g. 2 "date" Date { 2026-01-15 }
// e.g. 3 "formula" 100 (cell.formula = "=SUM(A1:A10)")
// e.g. 4 "covered" null (part of a merged cell)
}
}
}
// HTML table
const html = odsToHtml(bytes);
// Fast mode — values only, no formatting
const model2 = readOds(bytes, { includeFormatting: false });| Type | value |
Notes |
|---|---|---|
"string" |
string |
|
"float" |
number |
Includes percentage and currency cells |
"date" |
Date (UTC) |
|
"boolean" |
boolean |
|
"formula" |
cached result | cell.formula has original string e.g. "=SUM(A1:A10)" |
"empty" |
null |
|
"covered" |
null |
Covered by a merge — correct colIndex always maintained |
Primary cells have colSpan and/or rowSpan. Covered cells have type: "covered", value: null, and the correct physical colIndex — no offset confusion.
// A1:C1 merged — reading row 0:
// cell 0: { type: "string", value: "Header", colSpan: 3 }
// cell 1: { type: "covered", value: null, colIndex: 1 }
// cell 2: { type: "covered", value: null, colIndex: 2 }
// cell 3: { type: "string", value: "D1", colIndex: 3 } ← always correctodf-kit/xlsx converts .xlsx spreadsheets to .ods with no external dependencies — parses XLSX XML directly using fflate (already in odf-kit) and our own XML parser. Supports .xlsx and .xlsm. Does not support legacy .xls (binary format).
import { xlsxToOds } from "odf-kit/xlsx"
import { readFileSync, writeFileSync } from "fs"
// Simple conversion
const bytes = await xlsxToOds(readFileSync("report.xlsx"))
writeFileSync("report.ods", bytes)
// With options
const bytes2 = await xlsxToOds(readFileSync("report.xlsx"), {
dateFormat: "DD/MM/YYYY",
metadata: { title: "Q4 Report", creator: "Alice" },
})
// Works with ArrayBuffer too (browser-friendly)
const bytes3 = await xlsxToOds(arrayBuffer)What is preserved:
- All sheets in tab order, with their names
- Cell values: strings, numbers, booleans, dates, formula cached results
- Formula strings
- Merged cells (colSpan/rowSpan)
- Freeze rows/columns
- Multiple sheets
What is not preserved (out of scope for v0.9.9):
- Cell formatting (colors, fonts, borders)
- Column widths and row heights
- Charts, images, pivot tables
import { odtToTypst, modelToTypst } from "odf-kit/typst";
const typst = odtToTypst(readFileSync("letter.odt"));
writeFileSync("letter.typ", typst);
execSync("typst compile letter.typ letter.pdf");import { docxToOdt } from "odf-kit/docx"
const { bytes, warnings } = await docxToOdt(input, options?)
interface DocxToOdtOptions {
pageFormat?: "A4" | "letter" | "legal" | "A3" | "A5";
orientation?: "portrait" | "landscape";
preservePageLayout?: boolean; // default: true — read layout from DOCX
styleMap?: Record<string, number>; // custom style name → heading level
metadata?: { title?: string; creator?: string; description?: string };
}
interface DocxToOdtResult {
bytes: Uint8Array; // the .odt file
warnings: string[]; // content that could not be fully converted
}function htmlToOdt(html: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
function markdownToOdt(markdown: string, options?: HtmlToOdtOptions): Promise<Uint8Array>
interface HtmlToOdtOptions {
pageFormat?: "A4" | "letter" | "legal" | "A3" | "A5"; // default: "A4"
orientation?: "portrait" | "landscape";
marginTop?: string;
marginBottom?: string;
marginLeft?: string;
marginRight?: string;
metadata?: { title?: string; creator?: string; description?: string };
}function tiptapToOdt(json: TiptapNode, options?: TiptapToOdtOptions): Promise<Uint8Array>
interface TiptapNode {
type: string;
text?: string;
attrs?: Record<string, unknown>;
content?: TiptapNode[];
marks?: TiptapMark[];
}
interface TiptapMark {
type: string;
attrs?: Record<string, unknown>;
}
interface TiptapToOdtOptions extends HtmlToOdtOptions {
images?: Record<string, Uint8Array>;
unknownNodeHandler?: (node: TiptapNode, doc: OdtDocument) => void;
}| Method | Description |
|---|---|
setMetadata(options) |
Set title, creator, description |
setPageLayout(options) |
Set page size, margins, orientation |
setHeader(content) |
Set page header (string or builder) |
setFooter(content) |
Set page footer (string or builder) |
addHeading(content, level?) |
Add heading (level 1–6) |
addParagraph(content, options?) |
Add paragraph (string or builder) |
addTable(content, options?) |
Add table (string[][] or builder) |
addList(content, options?) |
Add list (string[] or builder) |
addImage(data, options) |
Add standalone image |
addPageBreak() |
Insert page break |
save() |
Generate .odt as Promise<Uint8Array> |
| Method | Description |
|---|---|
doc.setMetadata(options) |
Set title, creator, description |
doc.setDateFormat(format) |
Set default date display format |
doc.addSheet(name) |
Add a sheet tab — returns OdsSheet |
doc.save() |
Generate .ods as Promise<Uint8Array> |
sheet.addRow(values, options?) |
Add a row of cells |
sheet.setColumnWidth(index, width) |
Set column width |
sheet.setRowHeight(index, height) |
Set row height |
sheet.freezeRows(N?) |
Freeze top N rows (default 1) |
sheet.freezeColumns(N?) |
Freeze left N columns (default 1) |
sheet.setTabColor(color) |
Set sheet tab color |
function fillTemplate(templateBytes: Uint8Array, data: TemplateData): Uint8Array| Syntax | Description |
|---|---|
{tag} |
Replace with value |
{object.property} |
Dot notation |
{#tag}...{/tag} |
Loop or conditional |
{
bold?: boolean,
italic?: boolean,
fontSize?: number | string,
fontFamily?: string,
color?: string,
underline?: boolean,
strikethrough?: boolean,
superscript?: boolean,
subscript?: boolean,
highlightColor?: string,
}| Platform | Support |
|---|---|
| Node.js 22+ | ✅ Full |
| Chrome, Firefox, Safari, Edge | ✅ Full |
| Deno, Bun | ✅ Full |
| Cloudflare Workers | ✅ Full |
ESM only. Zero Node-specific APIs in the library source — enforced at the TypeScript level.
ODF is the ISO standard (ISO/IEC 26300) for documents. It's the default format for LibreOffice, mandatory for many governments and public sector organisations, and the best choice for long-term document preservation.
- Two runtime dependencies — fflate (ZIP) and marked (Markdown parsing). No transitive dependencies.
- Spec-compliant output — every generated file passes the OASIS ODF validator. Enforced on every commit by CI.
- Multiple ODF formats — ODT documents and ODS spreadsheets from the same library.
- Nine complete capability modes — build ODT, build ODS, convert HTML→ODT, convert Markdown→ODT, convert TipTap JSON→ODT, convert DOCX→ODT, fill templates, read, convert to Typst/PDF.
- TipTap/ProseMirror integration — direct JSON→ODT conversion for any TipTap-based editor, no intermediate HTML step.
- Zero-dependency Typst emitter — the only JavaScript library with built-in ODT→Typst conversion for PDF generation.
- TypeScript-first — full types across all sub-exports.
- Apache 2.0 — use freely in commercial and open source projects.
| Feature | odf-kit | simple-odf | docxtemplater |
|---|---|---|---|
| Generate .odt from scratch | ✅ | ❌ | |
| Generate .ods from scratch | ✅ merged cells, freeze, number formats, hyperlinks | ❌ | ❌ |
| Convert HTML → ODT | ✅ | ❌ | ❌ |
| Convert Markdown → ODT | ✅ | ❌ | ❌ |
| Convert TipTap JSON → ODT | ✅ | ❌ | ❌ |
| Convert DOCX → ODT | ✅ native, browser-safe | ❌ | ❌ |
| Fill .odt templates | ✅ | ❌ | ✅ .docx only |
| Read .odt files | ✅ | ❌ | ❌ |
| Convert to HTML | ✅ | ❌ | ❌ |
| Convert to Typst / PDF | ✅ | ❌ | ❌ |
| Browser support | ✅ | ❌ | ✅ |
| Maintained | ✅ | ❌ abandoned 2021 | ✅ |
| Open source | ✅ Apache 2.0 | ✅ MIT |
odf-kit targets ODF 1.2 (ISO/IEC 26300). Generated files include proper ZIP packaging, manifest, metadata, and all required namespace declarations. The OASIS ODF validator runs on every push via GitHub Actions.
v0.12.3 — ODT settings.xml added. 1113 tests passing.
v0.12.0 — lexicalToOdt() via odf-kit/lexical. Converts Lexical SerializedEditorState to ODT. CellBuilder.addLink(), addLineBreak(), addImage(). ODS freeze panes fixed. 1107 tests passing.
v0.11.0 — odtToMarkdown() and modelToMarkdown() via odf-kit/markdown. GFM and CommonMark flavors. 1078 tests passing.
v0.10.4 — ODS freeze fix: ViewId and ActiveTable added to settings.xml. typesVersions restored (dropped in v0.10.3). 1059 tests passing.
v0.10.3 — module-sync exports condition for bundler compatibility. typesVersions for TypeScript moduleResolution: node compatibility.
v0.10.2 — ODS freeze rows/columns fix — ActiveSplitRange and all split axis items now correctly emitted in settings.xml.
v0.10.0 — docxToOdt() via odf-kit/docx. Native DOCX→ODT converter — pure ESM, zero new dependencies, browser-safe. Preserves text, headings, formatting, tables, lists, images (actual dimensions), hyperlinks, bookmarks, footnotes, page layout, headers/footers, and tracked changes. Spec-validated against ECMA-376 5th edition. 1053 tests passing.
v0.9.9 — xlsxToOds() via odf-kit/xlsx. XLSX→ODS conversion with zero new dependencies. 936 tests passing.
v0.9.8 — ODS reader: readOds() and odsToHtml() via odf-kit/ods-reader. Typed values, formula strings, merged cell handling, formatting, metadata. odf-kit/odt-reader alias added. 889 tests passing.
v0.9.7 — ODS enhancements: number formats (integer, decimal:N, percentage, currency), merged cells (colSpan/rowSpan), freeze rows/columns, hyperlinks in cells, sheet tab color. 849 tests passing.
v0.9.6 — tiptapToOdt(): TipTap/ProseMirror JSON→ODT conversion. TiptapNode, TiptapMark, TiptapToOdtOptions types. unknownNodeHandler for custom extensions. Image support via pre-fetched bytes map. 817 tests passing.
v0.9.5 — markdownToOdt(): Markdown→ODT via marked + htmlToOdt. 786 tests passing.
v0.9.4 — ODS datetime auto-detection (nonzero UTC time → datetime format). ODS formula xmlns:of namespace fix (Err:510 resolved).
v0.9.2 — htmlToOdt(): HTML→ODT conversion with page format presets, full inline formatting, lists, tables, blockquote, pre, hr, and inline CSS. 769 tests passing.
v0.9.0 — ODS spreadsheet generation: OdsDocument, multiple sheets, auto-typed cells, formulas, date formatting, row and cell formatting, column widths, row heights. 707 tests passing.
v0.8.0 — odf-kit/typst: odtToTypst() and modelToTypst(). Zero-dependency ODT→Typst emitter for PDF generation.
v0.7.0 — Tier 3 reader: paragraph styles, page geometry, headers/footers, sections, tracked changes (all three ODF modes).
v0.6.0 — Tier 2 reader: span styles, image float/wrap, footnotes/endnotes, bookmarks, fields, cell/row styles.
v0.5.0 — odf-kit/reader: readOdt(), odtToHtml(). Tier 1 parsing.
v0.3.0 — Template engine: loops, conditionals, dot notation, automatic XML fragment healing.
v0.1.0 — Programmatic ODT creation: text, tables, page layout, lists, images, links, bookmarks.
- Generate ODT files in Node.js
- Generate ODT files in the browser
- Fill ODT templates in JavaScript
- Convert ODT to HTML in JavaScript
- Convert DOCX to ODT in JavaScript
- LibreOffice headless alternative
- SheetJS alternative for ODF
- ODT to PDF via Typst
- Generate ODT without LibreOffice
- ODF government compliance
- simple-odf alternative
- docxtemplater alternative for ODF
- ODT JavaScript ecosystem
- Free DOCX to ODT converter (online tool)
- Free ODT to Markdown converter (online tool)
- Free ODT to HTML converter (online tool)
- Free ODT to PDF converter (online tool)
- Free XLSX to ODS converter (online tool)
- Free Markdown to ODT converter (online tool)
- Free HTML to ODT converter (online tool)
- Free ODS to HTML converter (online tool)
Issues and pull requests welcome at github.com/GitHubNewbie0/odf-kit.
git clone https://github.com/GitHubNewbie0/odf-kit.git
cd odf-kit
npm install
npm run build
npm testApache 2.0 — see LICENSE for details.