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 folderLazy 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:
pnpm add docxIf 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
-
Partition into sections. The walker scans the top-level children of
<Document>. Anything outside a<DocSection>becomes the first section (using the document’s ownpageSize/orientation/margins). Each<DocSection>starts a new section with its own page setup. -
Walk each section’s children. Block primitives (
<Heading>,<Paragraph>,<DocList>,<DocTable>,<DocImage>, etc.) become one or moreParagraph/Tableinstances indocx. -
Flatten inline children. Inside a
ParagraphorHeading, the walker descends into the children: strings →TextRun,<DocText>→TextRunwith options,<DocLink>→ExternalHyperlinkwrapping styled runs, plus support for<br>tags. Unknown wrapper components are descended into transparently. -
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.
-
Pack and download.
docx.Packer.toBlob()produces the.docxbuffer; 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.