Guide · Browser · No Server Required

Generate ODF documents in the browser with JavaScript

Create .odt files entirely on the client side. No server, no LibreOffice installation, no backend processing. User data never leaves the browser — ideal for privacy-sensitive applications, offline tools, and lightweight document export.

Why generate documents in the browser?

Traditional document generation requires a server: your web application collects data, sends it to a backend, the backend generates a file, and sends it back for download. This works, but it adds complexity, latency, server cost, and — critically — it means user data must travel over the network.

Client-side generation eliminates all of that. The document is created right in the user's browser tab using JavaScript. The file never touches a server. This matters for:

What you'll build

By the end of this guide, you'll have a web page with a button that generates a formatted .odt document and downloads it — entirely in the browser. The document will include headings, formatted text, a table, and a list. It opens in LibreOffice, Google Docs, Microsoft Word, and any ODF-compliant application.

Prerequisites

If you don't have a project yet, the fastest way to start is with Vite:

# Create a new Vite project
npm create vite@latest my-odt-app -- --template vanilla
cd my-odt-app
npm install

Step 1 — Install odf-kit

$ npm install odf-kit

odf-kit has zero transitive runtime dependencies. It adds minimal weight to your bundle — no XML parsers, no heavy frameworks.

Step 2 — Generate a document and trigger a download

Here's the complete pattern for creating an .odt file in the browser and downloading it:

import { OdtDocument } from "odf-kit";

async function generateDocument() {
  const doc = new OdtDocument();

  doc.addHeading("Quarterly Report", 1);
  doc.addParagraph("Generated entirely in the browser.");

  doc.addTable([
    ["Region",  "Revenue",  "Growth"],
    ["North",   "$2.1M",    "+12%"],
    ["South",   "$1.8M",    "+8%"],
  ], { border: "0.5pt solid #000" });

  const bytes = await doc.save();

  // Create a download link
  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 = "report.odt";
  a.click();
  URL.revokeObjectURL(url);
}

// Wire it to a button
document.querySelector("#generate-btn")
  .addEventListener("click", generateDocument);

That's it. When the user clicks the button, a valid .odt file is generated in memory, converted to a Blob, and downloaded. No network requests. No server. The data exists only in the browser tab.

Step 3 — Fill a template from a file input

If you have an existing .odt template created in LibreOffice, you can let the user upload it and fill it with data — entirely client-side:

import { fillTemplate } from "odf-kit";

const input = document.querySelector("#template-file");

input.addEventListener("change", async (e) => {
  const file = e.target.files[0];
  const buffer = new Uint8Array(
    await file.arrayBuffer()
  );

  const result = fillTemplate(buffer, {
    customer: "Acme Corp",
    date: new Date().toLocaleDateString(),
    items: [
      { product: "Widget", qty: 5, price: "$125" },
      { product: "Gadget", qty: 3, price: "$120" },
    ],
  });

  // Download the filled document
  const blob = new Blob([result], {
    type: "application/vnd.oasis.opendocument.text",
  });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = "invoice.odt";
  a.click();
  URL.revokeObjectURL(url);
});

The template file is read locally with the File API. It never leaves the browser. The fillTemplate() function replaces all {placeholders} with your data, including loops ({#items}...{/items}) and conditionals. The filled document downloads immediately.

Step 4 — Add images from fetch or file input

In the browser, you load images with fetch() instead of fs.readFile():

const response = await fetch("logo.png");
const logoBytes = new Uint8Array(
  await response.arrayBuffer()
);

doc.addImage(logoBytes, "image/png", {
  width: "5cm",
  height: "2cm",
});

You can also get image bytes from a <canvas> element, a user-uploaded file, or any other source that produces a Uint8Array or ArrayBuffer.

How it works under the hood

An .odt file is a ZIP archive containing XML files and embedded resources. odf-kit builds the XML in memory, compresses it with fflate (a fast, pure-JavaScript ZIP library), and returns the result as a Uint8Array. There are no Node.js-specific APIs anywhere in the library — the TypeScript build enforces this at compile time. The same code runs in Node.js, browsers, Deno, Bun, and Cloudflare Workers.

The Blob + URL.createObjectURL() pattern is the standard way to trigger file downloads in the browser without a server. The browser creates a temporary URL pointing to the in-memory data, the <a> element triggers the download dialog, and revokeObjectURL() frees the memory.

Browser compatibility

odf-kit works in all modern browsers: Chrome, Firefox, Safari, and Edge. It uses only standard web APIs — TextEncoder, Uint8Array, Blob, URL.createObjectURL() — that are available in all browsers shipped since 2018. No polyfills needed.

Complete example — "Export as .odt" button

Here's a realistic example: a web application that lets users generate a formatted report and download it as an .odt file:

import { OdtDocument } from "odf-kit";

async function exportReport(data) {
  const doc = new OdtDocument();

  doc.setMetadata({ title: data.title });
  doc.setFooter("Page ###");

  doc.addHeading(data.title, 1);
  doc.addParagraph(`Generated on ${new Date().toLocaleDateString()}`);

  doc.addHeading("Summary", 2);
  doc.addParagraph(data.summary);

  doc.addHeading("Data", 2);
  doc.addTable(
    [data.columns, ...data.rows],
    { border: "0.5pt solid #000" }
  );

  doc.addHeading("Notes", 2);
  doc.addList(data.notes);

  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 = `${data.title}.odt`;
  a.click();
  URL.revokeObjectURL(url);
}

// Use it with your application data
exportReport({
  title: "Q4 Sales Report",
  summary: "Revenue exceeded targets in all regions.",
  columns: ["Region", "Revenue", "Growth"],
  rows: [
    ["North", "$2.1M", "+12%"],
    ["South", "$1.8M", "+8%"],
    ["East",  "$1.5M", "+15%"],
  ],
  notes: [
    "All figures are unaudited",
    "Growth is year-over-year",
  ],
});

When to use browser generation vs. server generation

Client-side generation is ideal when you want to keep data private, reduce server load, or build offline-capable tools. Server-side generation (Node.js) is better when you need to batch-generate hundreds of documents, integrate with databases, or run in a CI/CD pipeline. odf-kit uses the same API in both environments — the only difference is how you read input files and write output files.

What about .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 including the European Union, Germany, France, Italy, the Netherlands, the United Kingdom, Brazil, and India. If your users work with LibreOffice, or your application serves public sector organizations, .odt is the right choice. The files also open in Google Docs and Microsoft Word.

Also works server-side

The same API works in Node.js 18+, Deno, Bun, and Cloudflare Workers. See the Generate .odt Files in Node.js guide for server-side examples, or the Fill .odt Templates with JavaScript guide for the template engine.

Start generating .odt files in the browser

$ npm install odf-kit