Skip to content

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 .pptx file (browser download or node file write). Returns the saved filename.
  • deckToPptxBlob(deck) — returns a Blob with 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.ts
async 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:

  1. Look up CSS variables against document.documentElement.
  2. Paint the resolved CSS color onto a 1×1 hidden canvas.
  3. Read back ctx.fillStyle (browsers normalize to #rrggbb or rgb()).
  4. 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.