OneApp Docs
PackagesEditors

@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-editor

Slide 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-editor

2. Create a basic presentation

app/presentation/page.tsx
"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

app/presentation/page.tsx
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-editor

Build configuration: Uses tsdown with createDistConfig('react', ...) for distribution builds.


Technical Details

For Developers: Technical implementation details

Overview

PropertyValue
Locationpackages/pptx-editor
Dependenciespptxgenjs, react-dnd
Export FormatPPTX, PDF, PNG
Bundle Size~80KB (with tree-shaking)

Export Paths

PathDescription
@repo/pptx-editorMain editor component
@repo/pptx-editor/slidesSlide components
@repo/pptx-editor/exportExport 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, curtain

Element 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

ShortcutAction
Arrow Right/DownNext slide
Arrow Left/UpPrevious slide
HomeFirst slide
EndLast slide
FToggle fullscreen
EscapeExit fullscreen/presenter
NToggle notes
TReset timer

Editor Mode

ShortcutAction
Ctrl/Cmd + DDuplicate slide
Ctrl/Cmd + NNew slide
DeleteDelete element
Ctrl/Cmd + CCopy element
Ctrl/Cmd + VPaste element
Ctrl/Cmd + ZUndo
Ctrl/Cmd + YRedo
Ctrl/Cmd + SSave presentation
Ctrl/Cmd + EExport 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

External Resources

On this page