Skip to content

Exporting

exportToDocx(<Document />) is the only API surface most apps need. It takes a React element, walks the tree, builds a docx.Document, packs it into a Blob, and triggers a browser download.

import { exportToDocx } from '@helixui/document';
await exportToDocx(<Brief />);
// → brief.docx, downloads to the browser's default folder

Lazy import

The docx library is not bundled with @helixui/document. The export module only import('docx')s when you actually call exportToDocx / documentToDocxBlob. This keeps the base bundle tiny — useful when most of your users never hit the export path.

The library is declared as an optionalPeerDependency. Install it once at the app level:

Terminal window
pnpm add docx

If docx isn’t installed and the user clicks Export, the dynamic import throws and you’ll see the error in the console.

Options

await exportToDocx(<Brief />, {
fileName: 'brief-2026-q3.docx', // overrides Document's exportFileName
title: 'Q3 product brief',
author: 'platform team',
subject: 'Quarterly briefing',
description: 'Bets, owners, and risks for Q3 2026.',
keywords: ['helixui', 'product', 'q3'],
company: 'helixui',
});

Each metadata field overrides the matching one on <Document meta>. Use <Document meta> for static metadata; use options for per-export overrides (e.g., adding the user’s name to the author field).

Returning a Blob instead of downloading

If you want to upload the document, attach it to email, or hand it to a service worker, use documentToDocxBlob:

import { documentToDocxBlob } from '@helixui/document';
const blob = await documentToDocxBlob(<Brief />);
const formData = new FormData();
formData.append('file', blob, 'brief.docx');
await fetch('/api/uploads', { method: 'POST', body: formData });

The blob’s MIME type is application/vnd.openxmlformats-officedocument.wordprocessingml.document, which is what Word and most email clients expect.

How the walker works

  1. Partition into sections. The walker scans the top-level children of <Document>. Anything outside a <DocSection> becomes the first section (using the document’s own pageSize / orientation / margins). Each <DocSection> starts a new section with its own page setup.

  2. Walk each section’s children. Block primitives (<Heading>, <Paragraph>, <DocList>, <DocTable>, <DocImage>, etc.) become one or more Paragraph / Table instances in docx.

  3. Flatten inline children. Inside a Paragraph or Heading, the walker descends into the children: strings → TextRun, <DocText>TextRun with options, <DocLink>ExternalHyperlink wrapping styled runs, plus support for <br> tags. Unknown wrapper components are descended into transparently.

  4. Resolve colors via canvas. Theme keys, CSS variables, oklch(), rgb(), etc. are all converted to 6-character hex via an offscreen canvas. This means any DNA theme, any browser color function, and any helixui CSS variable resolves correctly in the export.

  5. Pack and download. docx.Packer.toBlob() produces the .docx buffer; the helper triggers an <a download> click.

Image inlining

Images are fetched at export time and embedded as base64 inside the file. The walker decides the image kind from the URL extension, the Content-Type header, or the data-URI MIME — whichever is available.

  • Raster (png, jpg, gif, bmp) — fetched as ArrayBuffer.
  • SVG — fetched as text and exported as a real SVG image, with a 1×1 transparent PNG as the raster fallback for legacy Word readers.
  • Data URIs are decoded inline; no network call.

CORS applies. The source must be reachable from the page that runs the export — local assets (/foo.png) work; cross-origin images need appropriate Access-Control-Allow-Origin headers.

Fonts

The default bodyFontSize is 11 points. You can change it via the theme:

<Document theme={{ bodyFontSize: 12 }}></Document>

Headings derive sizes from theme.headingFontSize (default 28pt). Each level scales it down: H1 1.0×, H2 0.75×, H3 0.6×, H4 0.52×, H5 0.46×, H6 0.42×. Override per-heading via the size prop.

font props ('heading' | 'body' | 'mono' | <css-family>) resolve through the theme. The DOM preview applies them as CSS font-family; the exporter strips the CSS variable wrapper and passes the first family in the stack to docx.

Reproducible exports

The walker is pure — same React tree → same .docx bytes (modulo non-deterministic image fetches). This is useful for:

  • Snapshot testing — diff two exports of the same document by unzipping and comparing the inner XML.
  • Cache friendly — if your data hasn’t changed, neither has the file. Hash the input data before deciding to regenerate.
  • CI generation — build documentation packs in CI and attach them as artifacts. The same component code drives the web preview and the artifact.

Server-side rendering

The exporter calls fetch, document.createElement, URL, and atob in the browser path. In Node 18+, fetch and atob are global; the canvas-based color resolver short-circuits to hex pass-through when no DOM is available.

For full server-side export from React (e.g. pre-rendering reports), use documentToDocxBlob, write the buffer to disk:

import { documentToDocxBlob } from '@helixui/document';
import { writeFile } from 'node:fs/promises';
const blob = await documentToDocxBlob(<Brief />);
await writeFile('brief.docx', Buffer.from(await blob.arrayBuffer()));

If you stick to inline assets and skip CSS-variable color references, the output will be byte-stable across runs.