Exporting to PPTX
import { exportToPptx, deckToPptxBlob } from '@helixui/slides';
await exportToPptx(<MyDeck />, { fileName: 'q1-review.pptx', author: 'HelixUI Demo', title: 'Q1 review',});That’s the entire surface. Two functions:
exportToPptx(deck, options?)— saves a.pptxfile (browser download or node file write). Returns the saved filename.deckToPptxBlob(deck)— returns aBlobwith the same content. Useful for uploads, email attachments, or piping into another system without prompting a download.
How it works
Every slide primitive has a static __helixuiSlideKind field — 'title',
'shape', 'chart', etc. The exporter walks the deck’s React tree once
and dispatches per kind:
switch (kind) { case 'frame': /* set bounds, descend into children */ case 'title': slide.addText(text, { …layoutBounds, fontSize, … }); case 'bullets': slide.addText(items, { bullet, indentLevel, … }); case 'shape': slide.addShape(prstName, { fill, line, rectRadius, … }); case 'image': slide.addImage({ data: dataUrl, x, y, w, h }); case 'table': slide.addTable(rows, { colW, fontFace, … }); case 'chart': slide.addChart(type, data, { showLegend, chartColors, … }); case 'notes': slide.addNotes(text);}Unknown components (an app’s <Stack>, for example) are walked
transparently — the exporter recurses into their children so layout
wrappers don’t break the export.
The lazy peer dependency
pptxgenjs is not part of the base @helixui/slides bundle. It is
imported only when you call exportToPptx (or deckToPptxBlob):
// inside packages/slides/src/export/pptx.tsasync function buildPresentation(deck) { const mod = await import('pptxgenjs'); const PptxGen = mod.default; // …}That means:
- A read-only deck preview (no export button) ships 0 KB of pptxgenjs.
- The first export call code-splits and loads pptxgenjs once. Subsequent exports reuse the cached module.
If pptxgenjs isn’t installed, exportToPptx rejects with the npm install
hint — DOM rendering still works.
Image inlining
In the browser, image URLs are fetched and converted to base64 data URLs
before being passed to pptx.addImage(). The result is a self-contained
.pptx — recipients don’t need network access to your image host.
// Roughly:const blob = await fetch(src).then((r) => r.blob());const dataUrl = await new Promise((resolve) => { const fr = new FileReader(); fr.onload = () => resolve(fr.result); fr.readAsDataURL(blob);});slide.addImage({ data: dataUrl, x, y, w, h });Cross-origin images need correct CORS headers. If the fetch fails, the
exporter falls back to passing the URL via path — pptxgenjs may resolve
it itself (works in node, hit-or-miss in the browser).
Theme bridging
SlideColor references — 'brand', 'success', '--helixui-color-...',
oklch(...) — are resolved to concrete hex during export. The
implementation:
- Look up CSS variables against
document.documentElement. - Paint the resolved CSS color onto a 1×1 hidden canvas.
- Read back
ctx.fillStyle(browsers normalize to#rrggbborrgb()). - Convert to a 6-char uppercase hex string.
Because step 2 is browser-native, anything CSS understands — including
oklch(), lab(), color-mix(), system colors — exports faithfully.
File metadata
await exportToPptx(<MyDeck />, { fileName: 'helixui-overview.pptx', author: 'helixui design system', title: 'helixui — slide primitives', company: 'helixui', subject: 'A walkthrough of @helixui/slides components', compression: true, // default});These land in the PPTX file’s core properties — visible under
File → Properties in PowerPoint or Get Info on macOS.
Server-side use
The walker is purely React tree traversal — it runs in node too. Color
resolution falls back to a best-effort path (hex literals work, CSS vars
default to their var(...) fallback) since there’s no DOM.
import ReactDOMServer from 'react-dom/server';import { deckToPptxBlob } from '@helixui/slides';
// In an API route:const blob = await deckToPptxBlob(<MyDeck />);return new Response(blob, { headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'Content-Disposition': 'attachment; filename="report.pptx"', },});What doesn’t survive (yet)
These are intentionally unsupported in v0:
- Slide transitions / animations. Out of scope; presentations should be durable artifacts, not playback engines.
- Speaker timings. Same reason.
- Complex text runs in one paragraph (mid-paragraph color changes).
Each
<SlideText>is one run; nest multiple if you need different formatting. - Embedded video / audio.
If you need any of these, drop down to the pptxgenjs instance directly via
the onPres / onSlide escape hatches. Both exportToPptx and
deckToPptxBlob accept these hooks; they fire after helixui has rendered the
React tree, so you can layer raw pptxgenjs calls on top.
await exportToPptx(<MyDeck />, { // Register custom slide masters, set extra metadata, etc. onPres: ({ pres }) => { pres.defineSlideMaster({ title: 'BRAND', objects: [/* ... */] }); }, // Add anything the typed surface can't express — video, audio, raw shapes. onSlide: ({ slide, index, element }) => { if (element.props.video) { slide.addMedia({ type: 'video', path: element.props.video, x: 0.5, y: 0.5, w: 9, h: 5, }); } },});The onSlide callback receives { slide, pres, index, total, element, size, theme };
element is the original <Slide> React element so you can read custom props.