Skip to content

Overview — @helix-ui/slides

@helix-ui/slides is a separate package in the helix-ui monorepo that lets you build presentations the same way you build the rest of your UI — as React components.

The mental model is simple:

A <Deck> is a container of <Slide>s. Each slide holds positioned children. Coordinates are in inches — the same unit PowerPoint uses — so what you see on screen lands in the .pptx exactly.

Live demo: /showcase/slides — build, navigate, and press ⬇ Export .pptx. Open the file in PowerPoint, Keynote, or Google Slides and it’ll be a real, editable presentation.

Install

Terminal window
pnpm add @helix-ui/slides
# Optional — only needed when you actually call the exporter.
pnpm add pptxgenjs

The pptxgenjs library is a lazy peer dependency. The base @helix-ui/slides bundle does not import it until you call exportToPptx(). If you only want the web preview, you don’t need to install it at all.

A 30-second deck

import {
Deck, Slide, Title, Subtitle, Bullets, Bullet, exportToPptx,
} from '@helix-ui/slides';
import '@helix-ui/slides/styles.css';
function Pitch() {
return (
<Deck size="16:9" exportFileName="pitch.pptx">
<Slide layout="title">
<Title>helix-ui slides</Title>
<Subtitle>tokens · components · pptx export</Subtitle>
</Slide>
<Slide layout="content">
<Title>Why this exists</Title>
<Bullets type="check">
<Bullet>Same tokens as the rest of helix-ui</Bullet>
<Bullet>Diffable, reviewable, programmatic</Bullet>
<Bullet>Native PPTX export — keeps charts editable</Bullet>
</Bullets>
</Slide>
</Deck>
);
}
// in a button handler
await exportToPptx(<Pitch />); // → pitch.pptx

How it maps to PPTX

Every primitive corresponds 1:1 to a PPTX construct so the export is predictable and lossless within the supported feature set.

ComponentPPTX equivalentNotes
<Slide>sldOne slide per element
<Frame>shape group boundsChildren inherit x / y / w / h
<Title> / <Subtitle>sp text bodyLayout placeholders kick in for blank decks
<Heading>sp text bodylevel={1..4} controls the size scale
<SlideText>sp text bodyFree-form runs
<Bullets> + <Bullet>sp text body w/ a:buChar/a:buAutoNumPer-item indent levels
<SlideShape>sp prstGeomTwenty preset shapes mapped 1:1
<SlideImage>picInlined as base64 in the browser
<SlideTable>graphicFrame tblStriped rows, colWidths honored
<SlideChart>graphicFrame chartNative PowerPoint chart, data-bound
<Notes>notesSldSpeaker notes pane

Why inches?

Every coordinate (x, y, w, h, radius, padding) is a number in inches. PowerPoint uses inches internally (well, EMUs — 914,400 per inch), and so does pptxgenjs. Using inches in the source means:

  • No coordinate translation. What you write is what gets exported.
  • Consistent rendering. The DOM renderer scales inches to CSS pixels (1in = 96px) and transform: scale()s the slide canvas to fit any container. A 1-inch shape is always 1 inch — at 100% zoom or 30%.
  • Print-ready math. Slide dimensions match the standard sizes: 16:9 → 13.333 × 7.5 in, 4:3 → 10 × 7.5 in.

Layouts vs. explicit Frames

Two ways to position content:

{/* 1. Use a layout — placeholders position semantic primitives */}
<Slide layout="content">
<Title>Click here.</Title>
<Bullets><Bullet>Anywhere.</Bullet></Bullets>
</Slide>
{/* 2. Use Frames — absolute positioning in inches */}
<Slide>
<Frame x={0.5} y={0.5} w={6} h={1}>
<Title>Pixel-perfect.</Title>
</Frame>
</Slide>

Both compose. A <Title> inside a <Frame> uses the frame’s bounds; a <Title> outside a frame falls into the layout’s title placeholder.

Theming

<Deck> accepts a theme prop with colors, fonts, and default font sizes. The defaults pull values from helix-ui CSS variables, so any DNA theme just flows through. During PPTX export, any token reference ('brand', '--helix-ui-color-...', oklch(...), etc.) is resolved to a concrete hex via a hidden canvas — your decks export faithfully under any theme.

<Deck
theme={{
colors: {
brand: 'oklch(0.62 0.18 264)',
ink: '#0b0d12',
paper: '#fafafa',
},
fonts: { heading: '"Geist", system-ui, sans-serif' },
titleFontSize: 56,
}}
>
</Deck>

Speaker mode

<Deck mode="single" controls> shows one slide at a time and binds:

  • / Space / PageDown — next
  • / PageUp — previous
  • Home / End — first / last

Pair it with <DeckThumbnails> for a navigable strip.

What’s next