npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-pptx-editor

v0.1.0

Published

A standalone React presentation editor — slides, text, shapes and images on a scalable canvas, with drag/resize/rotate, snapping, undo/redo, JSON persistence and PPTX import/export.

Readme

react-pptx-editor

A standalone React presentation editor: slides, text, shapes and images on a scalable canvas, with drag/resize/rotate, snapping, undo/redo, JSON persistence and PPTX import/export.

No backend, no authentication, no AI. You give it a presentation object and it gives you edits back.

npm install react-pptx-editor

React 18.2+ or 19 is a peer dependency.

The package is ESM-only — there is no CommonJS build. Any bundler used with React 18/19 handles ESM, and in Node it needs "type": "module" or a dynamic import(). The entry is also safe to import under SSR: nothing at module scope touches window, document or DOMParser, so importing it from a Next.js or Remix server render will not throw. The browser-only work is inside the calls that need it — see Limitations.


Usage

import { useState } from 'react';
import { PresentationEditor, createEmptyPresentation, type Presentation } from 'react-pptx-editor';
import 'react-pptx-editor/styles.css';

export function App() {
  const [doc, setDoc] = useState<Presentation>(() => createEmptyPresentation());

  return <PresentationEditor value={doc} onChange={setDoc} />;
}

The stylesheet is a separate entry point and is never injected automatically — import it once, wherever you import your other CSS.

value is a synchronising prop rather than a strictly-controlled one: the editor owns its internal state and reloads only when value's identity changes to something it did not itself emit. That keeps drag commits off your render path. For uncontrolled use, pass defaultValue instead.

Props

<PresentationEditor
  value={doc}                          // or defaultValue
  onChange={setDoc}                    // fires per committed mutation, not per frame
  editorRef={ref}                      // imperative handle, see below
  theme={{ accent: '#7C3AED' }}
  labels={{ addSlide: 'Neue Folie' }}
  assets={{ upload: uploadToS3 }}
  onError={(e) => toast(e.message)}
  toolbarActions={<MySaveButton />}    // extra controls on the right of the toolbar
  showSlideList={false}                // also showToolbar, showToolStrip, showElementToolbar
/>

Imperative access

const editorRef = useRef<Editor | null>(null);

<PresentationEditor defaultValue={doc} editorRef={editorRef} />;

editorRef.current?.addSlide();
editorRef.current?.undo();
const json = editorRef.current?.serialize(true);

Composing your own chrome

Every part is exported separately.

import { EditorProvider, SlideCanvas, SlideList, ElementToolbar, useEditor } from 'react-pptx-editor';

<EditorProvider defaultValue={doc} onChange={save}>
  <MyToolbar />          {/* useEditor() gives the full command surface */}
  <SlideList />
  <SlideCanvas />
  <ElementToolbar />
</EditorProvider>

Hooks: useEditor, useEditorState, usePresentation, useSlides, useSlide, useCurrentSlide, useCurrentSlideId, useSlideSize, useSelectedIds, useSelectedElements, useSingleSelection, useHistory, useZoom, useTool, useIsDirty.

useKeyboardShortcuts(editor, options) is exported for custom chrome. Pass scope — a ref to your editor's root element — if more than one editor can be on the page; without it the shortcuts are global, which is correct for a single editor. <PresentationEditor /> passes its own root automatically.

const rootRef = useRef<HTMLDivElement>(null);
useKeyboardShortcuts(editor, { scope: rootRef });

Headless

createEditor() has no React dependency and runs in Node with no DOM at all — useful for server-side validation, batch edits or generating decks. There is a test suite (tests/node.test.ts) that runs in a DOM-free environment to keep it that way.

import { createEditor, createTextElement } from 'react-pptx-editor';

const editor = createEditor({ initial: json });

editor.addSlide();
editor.addElement(createTextElement({ text: 'Hello' }, { x: 100, y: 100, width: 800, height: 120 }));

editor.transaction('layout', () => {          // one undo step for the whole batch
  editor.align(ids, 'center-x');
  editor.distribute(ids, 'y');
});

editor.undo();
const out = editor.serialize(true);

Capabilities

| Area | Supported | |---|---| | Slides | add, delete, duplicate, reorder (drag & drop), background colour | | Elements | text, shape (rectangle / circle / triangle / star), image | | Transform | drag, resize from 8 handles, rotate — all rotation-aware | | Constraints | Shift = axis-lock / aspect-lock / 15° rotation snap, Alt = resize from centre | | Snapping | element edges and centres, plus slide edges and centres, with live guides | | Selection | click, Shift-click, marquee, Ctrl+A; multi-element drag | | Layering | bring to front / forward / backward / send to back | | Align | left / centre / right / top / middle / bottom, distribute H and V | | Clipboard | copy, cut, paste (cross-slide), duplicate | | History | undo / redo, capped, transactional, with burst coalescing | | Text | inline editing, font, size, colour, weight, style, decoration, alignment | | View | fit-to-container plus manual zoom (10%–800%) | | Playback | fullscreen presenter with keyboard and swipe navigation | | Interchange | JSON, PPTX export, PPTX import |

Keyboard

| | | |---|---| | Ctrl/⌘ Z · Ctrl/⌘ Shift Z · Ctrl/⌘ Y | undo · redo · redo | | Ctrl/⌘ C · X · V · D | copy · cut · paste · duplicate | | Ctrl/⌘ A | select all on the current slide | | Delete · Backspace | delete selection | | Escape | clear selection, or cancel the gesture in flight | | Arrows · Shift+arrows | nudge 1 · nudge 10 | | Ctrl/⌘ ] · [ (+Shift) | forward · backward (front · back) | | Ctrl/⌘ + · - · 0 | zoom in · out · reset |

Shortcuts and image paste are bound to window, because the canvas and the chrome around it are not focusable. With two or more editors mounted, only the one whose root last received a pointer or focus event responds — clicking outside all of them leaves none active. A single editor is always active, so nothing changes for the common case.


The document model

Plain JSON — no classes, no cycles, no framework objects.

Presentation {
  version: number            // schema version, for migrations
  id: string
  title: string
  width: number              // logical slide size, default 1920x1080
  height: number
  theme: { backgroundColor, textColor, primaryColor, fontFamily }
  slides: Slide[]
}

Slide {
  id: string
  background: string         // any CSS background value
  elements: SlideElement[]
  metadata?: Record<string, unknown>   // yours; preserved, never read
}

SlideElement = TextElement | ImageElement | ShapeElement
// id, x, y, width, height   — top-left origin, slide units
// rotation                  — degrees clockwise about the centre
// zIndex                    — paint order (dense, 0..n-1)
// locked?                   — rendered but not selectable
// content                   — type-specific
// metadata?

SlideElement is a discriminated union on type, so content narrows without a cast:

if (element.type === 'text') element.content.fontSize;
if (element.type === 'shape') element.content.fillColor;

Elements are a flat array per slide — there is no grouping or nesting.


JSON persistence

The editor never talks to a backend. It hands you the document; where it goes is your decision.

<PresentationEditor
  value={doc}
  onChange={(next) => {
    setDoc(next);
    debouncedSave(next);        // localStorage, IndexedDB, REST, Supabase, S3…
  }}
/>

Helpers:

serializePresentation(doc, pretty?)   // → string
parsePresentation(json)               // → Presentation
normalizePresentation(unknown)        // never throws; repairs and fills defaults
validatePresentation(unknown)         // → ValidationIssue[]  (report without loading)
clonePresentation(doc)

normalizePresentation is the load-time boundary: it fills defaults, drops unknown element types, repairs duplicate IDs, clamps invalid numbers and guarantees at least one slide. Anything can be handed to it safely.


Assets

Image upload belongs to your application. Leave assets undefined and images become data: URIs — fine for a demo, bad for real documents. Provide a handler and you get URLs instead.

const assets: AssetHandler = {
  upload: async (file) => {
    const { url, width, height } = await uploadToCdn(file);
    return { url, width, height };     // width/height set the initial aspect ratio
  },
  resolve: (url) => signUrl(url),      // rewrite at render time
  fetch: (url) => authedFetch(url),    // used by PPTX export; override for CORS
  accept: ['image/png', 'image/jpeg'],
};

Without fetch, cross-origin images that fail CORS are silently absent from exported decks.


PPTX

import { exportToPptx, importPptx, downloadBlob } from 'react-pptx-editor';

const blob = await exportToPptx(doc, { assets, onWarning: console.warn });
downloadBlob(blob, `${doc.title}.pptx`);

const imported = await importPptx(await file.arrayBuffer(), { title: file.name });
editorRef.current?.load(imported);

pptxgenjs and jszip are dynamically imported, so an app that never touches PPTX does not download them — your bundler puts them in separate lazy chunks.

Import reads slide dimensions, theme colours, placeholder geometry inherited from the layout and master, text runs with font/size/weight/colour/ alignment, pictures (embedded as data URIs) and simple autoshapes. It does not read groups, tables, charts, SmartArt, gradients, transitions or animations. The deck's own aspect ratio is preserved.

Export writes text, shapes, images and slide backgrounds with position, size, rotation and opacity. Text formatting is per-element, matching the model.

Both are browser APIs. importPptx needs DOMParser and throws a named error if none is available; exportToPptx decodes images through <canvas>, and in a DOM-less environment it reports each one through onWarning and writes the deck without them.


Limitations

  • No grouping. Elements are flat; multi-selection is a transient ID list.
  • No rich text. Formatting is per-element; mixed styling inside one text box is not representable, and PPTX import collapses a shape's runs to the first styled one.
  • Text boxes do not auto-size to their content.
  • PPTX round-trip is lossy in both directions.
  • Slide thumbnails render live DOM. Fine to roughly 50 slides; beyond that they want memoising or rasterising.
  • No accessible keyboard path for reaching an element without a pointer. Shortcuts work; discovery does not.
  • No PNG or PDF export.
  • The editing surface is browser-only. createEditor(), the model helpers and the whole package entry import and run in Node; rendering the components and both PPTX directions require DOM APIs.
  • ESM-only. No CommonJS build is published.

Known advisory

npm audit reports a high-severity DoS advisory in image-size, a transitive dependency of pptxgenjs. It does not affect browser consumers: pptxgenjs maps image-size to false in its browser field, so the code is stubbed out and never reaches a browser bundle (verified against the built output). There is no upstream fix available yet — [email protected] is the latest release, and the advisory covers all published image-size versions.


Development

npm install
npm run dev          # playground at http://localhost:5173
npm test             # 128 tests
npm run typecheck
npm run build        # playground bundle → dist-playground/
npm run build:lib    # library bundle    → dist/
npm run smoke        # import dist/index.js in Node — guards SSR safety

CI runs all of the above plus npm pack --dry-run on Node 20 and 22. The toolchain needs Node 20.19+; the published package's own floor is Node 18.

The playground under src/playground/ consumes the editor source directly and is the primary manual test environment. The library lives under src/editor/, with src/editor/index.ts as its intentional public boundary — anything not exported there (gesture internals, the selection frame, geometry helpers, the store instance) is implementation and may change without a major version.

See docs/ARCHITECTURE.md for the internals.


License

MIT