What you'll build
By the end of this guide, you'll have a Node.js script that generates a formatted .odt document with headings, paragraphs, a table, and styled text. The same code works in browsers too — see the browser usage section below. The output opens in LibreOffice, Google Docs, Microsoft Word, and any application that supports OpenDocument Format.
Prerequisites
- Node.js 18 or later (nodejs.org)
- A Node.js project with
package.json(runnpm init -yif you don't have one) - ESM enabled: add
"type": "module"to your package.json
Step 1 — Install odf-kit
odf-kit has a single runtime dependency (fflate for ZIP packaging) with zero transitive dependencies. Nothing else to install.
Step 2 — Create your first document
Create a file called generate.js and add the following:
import { OdtDocument } from "odf-kit"; import { writeFileSync } from "fs"; const doc = new OdtDocument(); doc.addHeading("My First ODF Document", 1); doc.addParagraph("This file was generated by Node.js."); const bytes = await doc.save(); writeFileSync("output.odt", bytes); console.log("Created output.odt");
Run it with node generate.js. Open output.odt in LibreOffice Writer — you'll see a heading and a paragraph. That's the minimal case: three lines to create a valid .odt file.
Step 3 — Add a table
Tables accept a simple array of arrays. The first row typically contains headers:
doc.addTable([ ["Product", "Quantity", "Price"], ["Widget", "50", "$4.99"], ["Gadget", "25", "$9.99"], ["Gizmo", "100", "$2.49"], ], { border: "0.5pt solid #000" });
For more control, use a builder callback to set column widths, cell backgrounds, and merged cells. See the full table documentation.
Step 4 — Format text
Plain strings create plain paragraphs. For formatting, use a builder callback:
doc.addParagraph((p) => { p.addText("Total revenue: "); p.addText("$1.2M", { bold: true, fontSize: 14, color: "#16a34a" }); });
Available formatting options include bold, italic, underline, strikethrough, superscript, subscript, font family, font size, text color, and highlight color. Both shorthand (bold: true) and explicit (fontWeight: "bold") forms work.
Step 5 — Add page layout, headers, and footers
doc.setMetadata({ title: "Quarterly Report" }); doc.setPageLayout({ orientation: "landscape" }); doc.setHeader("Acme Corp — Confidential"); doc.setFooter("Page ###"); // ### is replaced with the actual page number
Step 6 — Add images
import { readFileSync } from "fs"; const logo = readFileSync("logo.png"); doc.addImage(logo, "image/png", { width: "5cm", height: "2cm", });
Supported formats: PNG, JPEG, GIF, SVG, WebP, BMP, and TIFF. Images are embedded in the .odt ZIP file — no external references.
Complete example
Here's a full script that combines everything above into a formatted report:
import { OdtDocument } from "odf-kit"; import { writeFileSync } from "fs"; const doc = new OdtDocument(); doc.setMetadata({ title: "Q4 Report" }); doc.setFooter("Page ###"); doc.addHeading("Q4 2025 Results", 1); doc.addParagraph("Revenue exceeded expectations across all regions."); doc.addTable([ ["Region", "Revenue", "Growth"], ["North", "$2.1M", "+12%"], ["South", "$1.8M", "+8%"], ["East", "$1.5M", "+15%"], ["West", "$2.3M", "+10%"], ], { border: "0.5pt solid #000" }); doc.addHeading("Summary", 2); doc.addParagraph((p) => { p.addText("Status: "); p.addText("Approved", { bold: true, color: "green" }); }); const bytes = await doc.save(); writeFileSync("report.odt", bytes); console.log("Created report.odt");
What about templates?
If you already have a .odt file created in LibreOffice and want to fill it with data, odf-kit has a template engine too. See the Fill .odt Templates with JavaScript guide.
Browser usage
The same API works in the browser — no server required. Use any bundler (Vite, webpack, esbuild) and import as normal. The only difference is how you save the file:
import { OdtDocument } from "odf-kit"; const doc = new OdtDocument(); doc.addHeading("Generated in the Browser", 1); doc.addParagraph("No server involved."); const bytes = await doc.save(); // 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 = "document.odt"; a.click(); URL.revokeObjectURL(url);
This is useful for privacy-sensitive applications, offline tools, and anywhere you want to generate documents without sending data to a server. odf-kit works in Chrome, Firefox, Safari, and Edge.
Why .odt instead of .docx?
.odt is the OpenDocument Format — an ISO international standard (ISO/IEC 26300) that's vendor-independent and open. It's the default format for LibreOffice, and it's required by many governments, the European Union, and public sector organizations. If your users work with LibreOffice, or your project needs to comply with open format mandates, .odt is the right choice. If you need .docx, the docx package is a good option.