Skip to content

Multi-format exports — one tree, three surfaces

The single most under-used capability in helixui: the React tree you render to the DOM also exports to .pptx and .docx. Same components. Same tokens. Brand can’t drift across surfaces because there’s only one surface to drift from.

The canonical demo lives at /showcase/reports on the marketing site. This recipe is the recipe — how to wire it up in your own app.

The shape

import {
Deck, Slide, Title, Subtitle, Bullets, Bullet,
exportToPptx, deckToPptxBlob,
} from '@helixui/slides';
import {
Document, Heading, Paragraph, DocList, DocListItem,
exportToDocx, documentToDocxBlob,
} from '@helixui/document';
interface ReportData {
company: string;
quarter: 'Q1' | 'Q2' | 'Q3' | 'Q4';
year: number;
revenue: number;
highlights: { label: string; detail: string }[];
}
// 1) Build a Deck from the data. Renders as DOM; exports to PPTX.
function ReportDeck({ data }: { data: ReportData }) {
return (
<Deck>
<Slide layout="title">
<Title>{data.company} · {data.quarter} {data.year}</Title>
<Subtitle>Revenue ${data.revenue.toLocaleString()}</Subtitle>
</Slide>
<Slide layout="content">
<Title>What worked</Title>
<Bullets>
{data.highlights.map((h) => (
<Bullet key={h.label}>{h.label}{h.detail}</Bullet>
))}
</Bullets>
</Slide>
</Deck>
);
}
// 2) Build a Document from the same data. Renders as DOM; exports to DOCX.
function ReportDoc({ data }: { data: ReportData }) {
return (
<Document>
<Heading level={1}>{data.company} · {data.quarter} {data.year}</Heading>
<Paragraph>Revenue ${data.revenue.toLocaleString()} this quarter.</Paragraph>
<Heading level={2}>What worked</Heading>
<DocList type="disc">
{data.highlights.map((h) => (
<DocListItem key={h.label}>{h.label}{h.detail}</DocListItem>
))}
</DocList>
</Document>
);
}
// 3) Wire the export buttons.
export function ReportPage({ data }: { data: ReportData }) {
const deck = <ReportDeck data={data} />;
const doc = <ReportDoc data={data} />;
return (
<main>
{/* DOM rendering */}
{deck}
<div role="toolbar">
<button onClick={() => exportToPptx(deck)}>Download .pptx</button>
<button onClick={() => exportToDocx(doc)}>Download .docx</button>
<button onClick={async () => {
const blob = await deckToPptxBlob(deck);
await emailToCustomer(blob);
}}>Email deck</button>
</div>
</main>
);
}

That’s the whole pattern.

Why this matters

Most teams I’ve seen ship a quarterly report three ways:

  1. PowerPoint that someone in Finance maintains by hand. It gets stale; the brand colors don’t match the website’s; the data is copy-pasted from a SQL query that’s now wrong.
  2. PDF generated from the PowerPoint, with the same problems plus “we’ll fix it next quarter.”
  3. Web page generated from the latest dashboard data, but only visible to engineers who know the internal URL.

helixui collapses all three into one React tree. The PowerPoint is no longer maintained by hand — it’s a downstream of the same component that renders the dashboard.

What’s shared, what isn’t

The exporters use the same tokens as the DOM render — colors, fonts, spacing. The layout primitives differ because PPTX and DOCX have different rendering models:

SurfacePrimitives
DOMStack, Grid, Card, Text, … (@helixui/core)
PPTXDeck, Slide, Title, Bullets, Frame (@helixui/slides)
DOCXDocument, Heading, Paragraph, DocList, DocTable (@helixui/document)

You don’t render a <Card> to a PowerPoint slide. You render a <Slide> to both DOM and PPTX. The mental model is “choose the right shape for the surface; the tokens come along.”

Lazy loading

pptxgenjs and docx are heavy. helixui lazy-imports them:

import { exportToPptx } from '@helixui/slides';
// `pptxgenjs` is NOT in your initial bundle.
await exportToPptx(<MyDeck />);
// ↑ first call triggers the import. Subsequent calls reuse it.

Your web bundle pays nothing until the user clicks Export.

Escape hatches

For things the typed surface doesn’t cover (video, audio, raw shapes), exportToPptx accepts onPres / onSlide hooks. See exporting docs.

When this isn’t the right call

  • Static documents that never change. Just author them in Word / Pages directly. The React tree is for generated docs.
  • Pixel-perfect PDF. PPTX and DOCX export are token-fidelity, not pixel-fidelity. If you need the latter, render to a canvas and print.
  • Heavy charts. @helixui/slides<SlideChart> renders cleanly for the chart types it supports; for niche visualization use the onSlide hook + raw pptxgenjs.

Try it

/showcase/reports on the marketing site is a working version. View the source at apps/site/src/pages/showcase/Reports.tsx — ~400 lines including the data shape, the React tree, and the export buttons.

The markdown studio at /showcase/markdown is the editor version of the same idea: type markdown, render as DOM, export as either .pptx or .docx.