@repo/pptx-editor
PowerPoint-style presentation editor for creating slide decks. Drag-and-drop elements, themes, animations. Export to PPTX, PDF, PNG. Presenter mode with notes and timer.
Quick Start
Add presentation editor in 2 minutes:
pnpm add @repo/pptx-editorSlide templates, themes, PPTX export included. Skip to Quick Start →
Why @repo/pptx-editor?
Presentation creation requires specialized tools. PowerPoint needed for every deck. No web-based slide editor. Export to PPTX manually handled per project. Themes duplicated across apps. Element positioning coded from scratch. Animation setup inconsistent. Presenter mode reimplemented everywhere.
@repo/pptx-editor solves this with web-based PowerPoint-style editor, pre-built slide templates, and PPTX export.
Production-ready with title/content/image slides, themes, drag-and-drop elements, charts, animations, PPTX/PDF export, and presenter mode.
Use cases
- Business presentations — Create pitch decks, quarterly reviews, sales presentations
- Educational content — Course materials, lectures, training slides
- Marketing materials — Product demos, case studies, webinars
- Reports — Export presentations as PDF for sharing
- Web presentations — Present directly in browser with presenter mode
How it works
@repo/pptx-editor provides React components for slide editing:
import { PresentationEditor } from "@repo/pptx-editor";
function PresentationPage() {
const [presentation, setPresentation] = useState(initialPresentation);
return (
<PresentationEditor
presentation={presentation}
onChange={setPresentation}
/>
);
}Uses pptxgenjs for PPTX generation, react-dnd for drag-and-drop, canvas for PDF/PNG export, and React for UI components.
Key features
Slide templates — Title, content, image, two-column layouts
Themes — Corporate, modern, minimal with custom branding
Elements — Text boxes, shapes, charts, images with positioning
Animations — Slide transitions and element animations
Export — PPTX, PDF, PNG with metadata
Presenter mode — Notes, timer, dual-screen support
Quick Start
1. Install the package
pnpm add @repo/pptx-editor2. Create a basic presentation
"use client";
import { PresentationEditor } from "@repo/pptx-editor";
import { TitleSlide, ContentSlide } from "@repo/pptx-editor/slides";
import { useState } from "react";
export default function PresentationPage() {
const [presentation, setPresentation] = useState({
id: "pres_1",
title: "My Presentation",
slides: [
{ type: "title", title: "My First Deck", subtitle: "Created with @repo/pptx-editor" },
{ type: "content", title: "Key Points", content: ["Point 1", "Point 2", "Point 3"] },
],
});
return (
<PresentationEditor
presentation={presentation}
onChange={setPresentation}
/>
);
}3. Export to PowerPoint
import { exportToPptx } from "@repo/pptx-editor/export";
const handleExport = async () => {
const pptxBlob = await exportToPptx(presentation, {
author: "John Doe",
title: presentation.title,
company: "Acme Inc",
});
// Download file
const url = URL.createObjectURL(pptxBlob);
const a = document.createElement("a");
a.href = url;
a.download = "presentation.pptx";
a.click();
};
<button onClick={handleExport}>Export to PPTX</button>That's it! You now have a web-based presentation editor with PowerPoint export.
Presenter mode
Present with notes and timer:
import { PresenterView } from "@repo/pptx-editor";
<PresenterView
presentation={presentation}
showNotes={true}
showTimer={true}
/>Distribution
This package is available as @oneapp/pptx-editor for use outside the monorepo.
npm install @oneapp/pptx-editorBuild configuration: Uses tsdown with
createDistConfig('react', ...) for distribution builds.
Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | packages/pptx-editor |
| Dependencies | pptxgenjs, react-dnd |
| Export Format | PPTX, PDF, PNG |
| Bundle Size | ~80KB (with tree-shaking) |
Export Paths
| Path | Description |
|---|---|
@repo/pptx-editor | Main editor component |
@repo/pptx-editor/slides | Slide components |
@repo/pptx-editor/export | Export utilities |
Basic Usage
Simple Presentation
import { PresentationEditor } from "@repo/pptx-editor";
function PresentationPage() {
// highlight-next-line
const [presentation, setPresentation] = useState(initialPresentation);
return (
<PresentationEditor
presentation={presentation}
onChange={setPresentation}
/>
);
}With Toolbar
import { PresentationEditor, Toolbar } from "@repo/pptx-editor";
function FullEditor() {
const [presentation, setPresentation] = useState(initialPresentation);
return (
<div className="h-screen flex flex-col">
<Toolbar
onAddSlide={() => {/* Add slide logic */}}
onExport={() => {/* Export logic */}}
onPresent={() => {/* Presenter mode */}}
/>
<PresentationEditor
presentation={presentation}
onChange={setPresentation}
className="flex-1"
/>
);
}Controlled Slides
const [currentSlide, setCurrentSlide] = useState(0);
<PresentationEditor
presentation={presentation}
onChange={setPresentation}
currentSlide={currentSlide}
onSlideChange={setCurrentSlide}
/>
// Navigate programmatically
<button onClick={() => setCurrentSlide(currentSlide + 1)}>
Next Slide
</button>Slide Types
Title Slide
import { TitleSlide } from "@repo/pptx-editor/slides";
<TitleSlide
// highlight-start
title="My Presentation"
subtitle="Created with @repo/pptx-editor"
author="John Doe"
date={new Date().toLocaleDateString()}
// highlight-end
theme={theme}
/>Content Slide
import { ContentSlide } from "@repo/pptx-editor/slides";
<ContentSlide
title="Key Points"
// highlight-start
content={[
"First important point",
"Second important point",
"Third important point",
]}
// highlight-end
layout="bullets" // or "numbered"
theme={theme}
/>Image Slide
import { ImageSlide } from "@repo/pptx-editor/slides";
<ImageSlide
title="Visual Example"
// highlight-next-line
image="/path/to/image.png"
caption="Figure 1: Example diagram"
imagePosition="center" // left, center, right
theme={theme}
/>Two Column Slide
import { TwoColumnSlide } from "@repo/pptx-editor/slides";
<TwoColumnSlide
title="Comparison"
leftContent={
<div>
<h3>Before</h3>
<ul>
<li>Manual process</li>
<li>Time-consuming</li>
</ul>
}
rightContent={
<div>
<h3>After</h3>
<ul>
<li>Automated</li>
<li>Efficient</li>
</ul>
}
splitRatio={0.5} // 50/50 split
/>Blank Slide
import { BlankSlide } from "@repo/pptx-editor/slides";
// Start with empty canvas
<BlankSlide
onElementAdd={(element) => {
console.log("Added:", element);
}}
/>Themes
Built-in Themes
import { PresentationEditor, themes } from "@repo/pptx-editor";
// Corporate theme (blue/gray)
<PresentationEditor theme={themes.corporate} />
// Modern theme (bold colors)
<PresentationEditor theme={themes.modern} />
// Minimal theme (black/white)
<PresentationEditor theme={themes.minimal} />Custom Theme
// highlight-start
const customTheme = {
colors: {
primary: "#1a73e8",
secondary: "#ea4335",
background: "#ffffff",
text: "#202124",
accent: "#34a853",
},
fonts: {
heading: "Roboto",
body: "Open Sans",
code: "Roboto Mono",
},
spacing: {
slideMargin: 40,
elementPadding: 20,
},
layout: {
titleHeight: 80,
contentMargin: 60,
},
};
// highlight-end
<PresentationEditor theme={customTheme} />Theme Customization
import { themes } from "@repo/pptx-editor";
// Extend existing theme
const myTheme = {
...themes.corporate,
colors: {
...themes.corporate.colors,
primary: "#ff6b6b", // Override primary color
},
};
<PresentationEditor theme={myTheme} />Elements
Text Box
import { TextBox } from "@repo/pptx-editor";
<TextBox
// highlight-start
x={100} // pixels from left
y={100} // pixels from top
width={400}
height={200}
// highlight-end
content="Editable text content"
style={{
fontSize: 24,
fontWeight: "bold",
color: "#333",
textAlign: "center",
}}
editable={true}
onUpdate={(newContent) => {
console.log("Updated:", newContent);
}}
/>Shape
import { Shape } from "@repo/pptx-editor";
// Rectangle
<Shape
// highlight-next-line
type="rectangle"
x={50}
y={50}
width={100}
height={100}
fill="#3b82f6"
stroke="#1d4ed8"
strokeWidth={2}
opacity={0.8}
/>
// Circle
<Shape
type="circle"
x={200}
y={50}
radius={50}
fill="#10b981"
/>
// Triangle
<Shape
type="triangle"
x={350}
y={50}
width={100}
height={100}
fill="#f59e0b"
/>
// Arrow
<Shape
type="arrow"
x={500}
y={50}
width={150}
height={50}
fill="#ef4444"
direction="right" // left, right, up, down
/>Chart
import { Chart } from "@repo/pptx-editor";
// Bar chart
<Chart
// highlight-next-line
type="bar"
data={{
labels: ["Q1", "Q2", "Q3", "Q4"],
datasets: [
{
label: "Revenue",
data: [100, 150, 200, 180],
backgroundColor: "#3b82f6",
},
{
label: "Profit",
data: [20, 30, 50, 40],
backgroundColor: "#10b981",
},
],
}}
options={{
title: "Quarterly Performance",
legend: { position: "bottom" },
}}
/>
// Line chart
<Chart
type="line"
data={{
labels: ["Jan", "Feb", "Mar", "Apr", "May"],
datasets: [
{
label: "Users",
data: [100, 120, 150, 180, 200],
borderColor: "#3b82f6",
fill: false,
},
],
}}
/>
// Pie chart
<Chart
type="pie"
data={{
labels: ["Product A", "Product B", "Product C"],
datasets: [
{
data: [30, 50, 20],
backgroundColor: ["#3b82f6", "#10b981", "#f59e0b"],
},
],
}}
/>
// Donut chart
<Chart
type="donut"
data={{
labels: ["Mobile", "Desktop", "Tablet"],
datasets: [
{
data: [45, 40, 15],
backgroundColor: ["#3b82f6", "#8b5cf6", "#ec4899"],
},
],
}}
options={{
cutout: "70%",
}}
/>Image Element
import { ImageElement } from "@repo/pptx-editor";
<ImageElement
src="/path/to/image.png"
x={100}
y={100}
width={400}
height={300}
// highlight-start
fit="cover" // cover, contain, fill
opacity={1}
borderRadius={8}
// highlight-end
alt="Product screenshot"
onLoad={() => console.log("Image loaded")}
/>Table Element
import { TableElement } from "@repo/pptx-editor";
<TableElement
x={50}
y={100}
width={600}
data={[
["Product", "Q1", "Q2", "Q3", "Q4"],
["Widget A", "$100K", "$120K", "$150K", "$180K"],
["Widget B", "$80K", "$90K", "$110K", "$130K"],
["Widget C", "$60K", "$75K", "$85K", "$95K"],
]}
headerRow={true}
style={{
headerBackground: "#3b82f6",
headerColor: "#fff",
borderColor: "#ddd",
fontSize: 14,
}}
/>Export
To PPTX
import { exportToPptx } from "@repo/pptx-editor/export";
// highlight-start
const pptxBlob = await exportToPptx(presentation, {
author: "John Doe",
title: "My Presentation",
subject: "Quarterly Review",
company: "Acme Inc",
revision: "1.0",
createdDate: new Date(),
layout: "16:9" // or "4:3"
});
// highlight-end
// Download in browser
const url = URL.createObjectURL(pptxBlob);
const a = document.createElement("a");
a.href = url;
a.download = "presentation.pptx";
a.click();
// Upload to server
const formData = new FormData();
formData.append("file", pptxBlob, "presentation.pptx");
await fetch("/api/upload", {
method: "POST",
body: formData
});To PDF
import { exportToPdf } from "@repo/pptx-editor/export";
const pdfBlob = await exportToPdf(presentation, {
// highlight-start
quality: "high", // low, medium, high
pageSize: "16:9", // or "4:3"
compression: true,
// highlight-end
metadata: {
title: "My Presentation",
author: "John Doe"
}
});
// Download
const url = URL.createObjectURL(pdfBlob);
const a = document.createElement("a");
a.href = url;
a.download = "presentation.pdf";
a.click();To Images
import { exportToImages } from "@repo/pptx-editor/export";
const images = await exportToImages(presentation, {
format: "png", // png, jpeg
// highlight-next-line
scale: 2, // 2x resolution (retina)
quality: 0.95, // for JPEG
backgroundColor: "#ffffff"
});
// images is an array of Blobs, one per slide
images.forEach((imageBlob, index) => {
const url = URL.createObjectURL(imageBlob);
const a = document.createElement("a");
a.href = url;
a.download = `slide-${index + 1}.png`;
a.click();
});Export Current Slide
import { exportSlideToImage } from "@repo/pptx-editor/export";
const slideImage = await exportSlideToImage(presentation.slides[currentSlide], {
format: "png",
scale: 2
});Animations
Slide Transitions
import { Slide } from "@repo/pptx-editor";
// highlight-start
// Fade transition
<Slide transition="fade" transitionDuration={500}>
{/* slide content */}
</Slide>
// Slide from right
<Slide transition="slideRight" transitionDuration={300}>
{/* slide content */}
</Slide>
// Zoom in
<Slide transition="zoomIn" transitionDuration={400}>
{/* slide content */}
</Slide>
// highlight-end
// Available transitions:
// - fade, slideLeft, slideRight, slideUp, slideDown
// - zoomIn, zoomOut, flipHorizontal, flipVertical
// - wipe, dissolve, curtainElement Animations
<TextBox
content="Animated text"
// highlight-start
animation={{
type: "fadeIn", // fadeIn, slideIn, zoomIn, bounceIn
delay: 200, // milliseconds
duration: 500,
trigger: "onClick", // onClick, withPrevious, afterPrevious
easing: "easeInOut", // linear, easeIn, easeOut, easeInOut
}}
// highlight-end
/>
// Multiple animations
<Shape
type="rectangle"
animation={{
type: "slideIn",
direction: "left",
delay: 0,
duration: 400,
}}
/>Animation Sequences
import { AnimationSequence } from "@repo/pptx-editor";
<AnimationSequence>
<TextBox
content="First"
animation={{ type: "fadeIn", delay: 0 }}
/>
<TextBox
content="Second"
animation={{ type: "fadeIn", delay: 500 }}
/>
<TextBox
content="Third"
animation={{ type: "fadeIn", delay: 1000 }}
/>
</AnimationSequence>Presenter Mode
Basic Presenter View
import { PresenterView } from "@repo/pptx-editor";
<PresenterView
presentation={presentation}
currentSlide={currentSlide}
onSlideChange={setCurrentSlide}
// highlight-start
showNotes={true}
showTimer={true}
showNextSlide={true}
// highlight-end
/>With Notes
// Add notes to slides
const slideWithNotes = {
...slide,
notes: `
- Emphasize the growth in Q3
- Mention the new product launch
- Address any questions about revenue decline
`,
};
// Notes appear in presenter view
<PresenterView
presentation={presentation}
showNotes={true}
/>Timer Configuration
<PresenterView
presentation={presentation}
timer={{
// highlight-start
duration: 30 * 60, // 30 minutes in seconds
showWarningAt: 5 * 60, // Warning at 5 minutes remaining
onTimeout: () => {
alert("Time's up!");
},
// highlight-end
}}
/>Dual Screen Mode
import { openPresenterWindow } from "@repo/pptx-editor";
// Open presenter view in new window
const presenterWindow = openPresenterWindow(presentation, {
currentSlide,
onSlideChange: setCurrentSlide,
showNotes: true,
showTimer: true,
});
// Main window shows fullscreen presentation
<PresentationView
presentation={presentation}
currentSlide={currentSlide}
fullscreen={true}
/>Drag and Drop
Reorder Slides
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { SlideSorter } from "@repo/pptx-editor";
<DndProvider backend={HTML5Backend}>
<SlideSorter
slides={presentation.slides}
onReorder={(newOrder) => {
setPresentation({
...presentation,
slides: newOrder,
});
}}
/>
</DndProvider>Drag Elements
import { DraggableElement } from "@repo/pptx-editor";
<DraggableElement
element={textBox}
onDragEnd={(position) => {
updateElement({
...textBox,
x: position.x,
y: position.y,
});
}}
bounds={{
minX: 0,
minY: 0,
maxX: slideWidth,
maxY: slideHeight,
}}
/>Keyboard Shortcuts
Presentation Mode
| Shortcut | Action |
|---|---|
Arrow Right/Down | Next slide |
Arrow Left/Up | Previous slide |
Home | First slide |
End | Last slide |
F | Toggle fullscreen |
Escape | Exit fullscreen/presenter |
N | Toggle notes |
T | Reset timer |
Editor Mode
| Shortcut | Action |
|---|---|
Ctrl/Cmd + D | Duplicate slide |
Ctrl/Cmd + N | New slide |
Delete | Delete element |
Ctrl/Cmd + C | Copy element |
Ctrl/Cmd + V | Paste element |
Ctrl/Cmd + Z | Undo |
Ctrl/Cmd + Y | Redo |
Ctrl/Cmd + S | Save presentation |
Ctrl/Cmd + E | Export to PPTX |
Data Structure
Presentation Interface
interface Presentation {
id: string;
title: string;
// highlight-next-line
theme: Theme;
slides: Slide[];
metadata: {
author?: string;
company?: string;
subject?: string;
createdAt: string;
updatedAt: string;
};
}
interface Theme {
colors: {
primary: string;
secondary: string;
background: string;
text: string;
accent?: string;
};
fonts: {
heading: string;
body: string;
code?: string;
};
spacing?: {
slideMargin?: number;
elementPadding?: number;
};
layout?: {
titleHeight?: number;
contentMargin?: number;
};
}Slide Interface
interface Slide {
id: string;
type: SlideType; // "title" | "content" | "image" | "twoColumn" | "blank"
elements: Element[];
notes?: string;
transition?: Transition;
backgroundColor?: string;
}
interface Element {
id: string;
type: ElementType; // "text" | "shape" | "image" | "chart" | "table"
x: number;
y: number;
width: number;
height: number;
zIndex?: number;
animation?: Animation;
}
interface Transition {
type: string; // "fade" | "slide" | "zoom" | etc.
duration: number;
easing?: string;
}
interface Animation {
type: string; // "fadeIn" | "slideIn" | "zoomIn" | etc.
delay: number;
duration: number;
trigger: "onClick" | "withPrevious" | "afterPrevious";
direction?: "left" | "right" | "up" | "down";
easing?: string;
}Persistence
Save Presentation
import { savePresentation } from "@repo/pptx-editor";
// Save to database
await savePresentation(presentation, {
userId: currentUser.id,
folderId: currentFolder.id
});
// Auto-save
useEffect(() => {
const timer = setTimeout(() => {
savePresentation(presentation);
}, 5000); // Save after 5 seconds of inactivity
return () => clearTimeout(timer);
}, [presentation]);Load Presentation
import { loadPresentation } from "@repo/pptx-editor";
const presentation = await loadPresentation(presentationId);
setPresentation(presentation);Version History
import { savePresentationVersion, getPresentationHistory } from "@repo/pptx-editor";
// Save version
await savePresentationVersion(presentation, {
message: "Updated charts on slide 3"
});
// Get history
const versions = await getPresentationHistory(presentationId);
// Restore version
const previousVersion = versions[1];
setPresentation(previousVersion.data);Collaboration
Real-time Editing
import { useCollaborativePresentation } from "@repo/pptx-editor";
const { presentation, updatePresentation, collaborators } =
useCollaborativePresentation(presentationId, {
userId: currentUser.id,
onUserJoin: (user) => {
toast.info(`${user.name} joined`);
},
onUserLeave: (user) => {
toast.info(`${user.name} left`);
},
});
// Show active users
<div className="flex gap-2">
{collaborators.map((user) => (
<Avatar key={user.id} src={user.avatar} name={user.name} />
))}Commenting
import { Comment } from "@repo/pptx-editor";
<Slide>
{/* Slide content */}
<Comment
x={200}
y={150}
author={currentUser}
content="Should we use a different color here?"
createdAt={new Date()}
replies={[
{
author: otherUser,
content: "Good point, I'll update it",
createdAt: new Date(),
},
]}
/>
</Slide>Testing
Mock Presentation Editor
import { vi } from "vitest";
vi.mock("@repo/pptx-editor", () => ({
PresentationEditor: ({ presentation, onChange }) => (
<div data-testid="presentation-editor">
<button onClick={() => onChange({ ...presentation, title: "Updated" })}>
Update
</button>
),
exportToPptx: vi.fn().mockResolvedValue(new Blob()),
}));
describe("PresentationPage", () => {
it("renders editor", () => {
render(<PresentationPage />);
expect(screen.getByTestId("presentation-editor")).toBeInTheDocument();
});
it("exports to PPTX", async () => {
const { exportToPptx } = require("@repo/pptx-editor");
render(<PresentationPage />);
fireEvent.click(screen.getByText("Export"));
await waitFor(() => {
expect(exportToPptx).toHaveBeenCalled();
});
});
});Test Export
import { exportToPptx } from "@repo/pptx-editor/export";
describe("PPTX Export", () => {
it("generates valid PPTX blob", async () => {
const presentation = {
id: "test",
title: "Test",
slides: [{ type: "title", title: "Title", subtitle: "Subtitle" }]
};
const blob = await exportToPptx(presentation);
expect(blob).toBeInstanceOf(Blob);
expect(blob.type).toBe("application/vnd.openxmlformats-officedocument.presentationml.presentation");
expect(blob.size).toBeGreaterThan(0);
});
});Troubleshooting
Export Not Working
# 1. Verify pptxgenjs installation
pnpm list pptxgenjs
# 2. Check browser compatibility
# PPTX export requires modern browser with Blob support
# 3. Verify presentation structure
console.log(JSON.stringify(presentation, null, 2));Images Not Displaying
// ❌ WRONG - Relative path
<ImageSlide image="./image.png" />
// ✅ CORRECT - Absolute URL or data URI
<ImageSlide image="https://example.com/image.png" />
<ImageSlide image="/images/slide-image.png" />Themes Not Applying
// Ensure theme is passed to editor
<PresentationEditor
presentation={presentation}
theme={customTheme} // Must pass theme prop
/>
// Or set theme in presentation object
setPresentation({
...presentation,
theme: customTheme,
});Animations Not Playing
// Check if animations are enabled
<PresentationEditor
presentation={presentation}
enableAnimations={true} // Default: true
/>
// Verify animation configuration
<TextBox
animation={{
type: "fadeIn",
duration: 500, // Must be > 0
trigger: "onClick", // Valid trigger
}}
/>Performance Tips
- Lazy load slides for large presentations (100+ slides)
- Optimize images before adding to slides (compress, resize)
- Limit animations to essential elements
- Use theme variables instead of inline styles
- Cache exported PPTX for faster re-downloads
- Debounce auto-save to reduce database writes
Related Documentation
- @repo/editor-doc - Document editor
- @repo/storage - File storage
- @repo/ui - UI components