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 '@helix-ui/document';
await exportToDocx(<Brief />);
// → brief.docx, downloads to the browser's default folder

Lazy import

The docx library is not bundled with @helix-ui/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: ['helix-ui', 'product', 'q3'],
company: 'helix-ui',
});

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 '@helix-ui/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 helix-ui 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 '@helix-ui/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.