pptx

Store

Presentation store API reference.

The store is the single source of truth for all presentation state: loading, navigation, zoom, edits, and undo history. It's compatible with React's useSyncExternalStore.

Creating a store

Uncontrolled (default)

Presentation.Root creates an internal store automatically. This is the simplest setup:

<Presentation.Root file={file}>
  {/* descendants can use usePresentation(), useSlide(), useZoom(), useHistory() */}
</Presentation.Root>

Descendants can still drive that internal store: usePresentationStore() returns it, so a toolbar can call edit(), undo(), or save() without you owning the instance.

import { usePresentationStore } from "@diceui/pptx";

function SaveButton() {
  const store = usePresentationStore();
  return <button onClick={() => store.save()}>Save</button>;
}

It returns a stable reference and subscribes to nothing, so use usePresentation, useSlide, useZoom, or useHistory for reactive reads rather than store.getState().

Controlled

Use useCreatePresentationStore when the store has to exist before the tree does, for example to load a file from an event handler that lives outside it:

import { useCreatePresentationStore } from "@diceui/pptx";

const store = useCreatePresentationStore();

// then pass it to the provider
<Presentation.Provider store={store}>
  <Presentation.Root>...</Presentation.Root>
</Presentation.Provider>;

State

interface PresentationState {
  status: "idle" | "loading" | "ready" | "error";
  presentation: PresentationData | null;
  activeSlideId: string | null;
  zoom: number; // default: 1
  isAutoFit: boolean; // whether zoom tracks the viewport size
  progress: number; // 0–100, meaningful while status === "loading"
  error: Error | null;
  revision: number; // bumped on every edit/undo/redo
}

revision is important: presentation is mutated in place by edits, so its object identity never changes. Subscribe to revision to react to content changes.

Methods

load(input, options?)

store.load(file, {
  defaultSlideIndex?: number | ((slides) => number), // default: 0
  defaultZoom?: number,  // initial zoom; auto-fit overrides it on mount
  readOnly?: boolean,    // default: true
  lazy?: boolean,        // default: true (parse slides on demand)
  embedFonts?: boolean,  // default: true (decode embedded fonts before ready)
})

Parses a File, Blob, ArrayBuffer, or Uint8Array. Returns a promise that resolves to PresentationData. A superseded load rejects with DOMException("AbortError") (safe to ignore).

reset()

Clears the store back to the initial idle state and aborts any in-flight load.

store.goTo(slideId: string)   // by stable slide id
store.goToIndex(index: number) // 0-based, clamped to valid range
store.next()
store.prev()
store.getActiveSlide() // SlideData | null
store.canGoNext()
store.canGoPrev()

Zoom

store.setZoom(n: number)      // clamped to [0.1, 4]; releases auto-fit
store.zoomIn(step?: number)   // default step: 0.25; releases auto-fit
store.zoomOut(step?: number)
store.setAutoFit(isAutoFit: boolean) // fit now and on every resize
store.fitTo(width, height, padding?) // one-shot fit for a known box

Measuring is a viewport concern, deciding is the store's. state.isAutoFit says whether zoom is tracking the container, and a mounted <Presentation.Viewport autoFit> refits itself whenever it is on. setZoom, zoomIn, and zoomOut turn it off so the level someone picked survives the next resize; setAutoFit(true) turns it back on and refits immediately. fitTo is the escape hatch for a box you measured yourself, and leaves the mode alone.

isAutoFit describes the viewport rather than the deck, so it survives load() and reset().

The zoomChange event reports what produced the change, which is how a control tells a resize-driven fit apart from a level someone asked for:

store.on("zoomChange", ({ zoom, previousZoom, reason }) => {
  // reason: "zoom" | "fit" | "load" | "reset"
});

Editing

Requires the deck to have been loaded with { readOnly: false }.

await store.edit(op: EditOperation): Promise<EditResult>
store.undo(): boolean
await store.redo(): Promise<boolean>
store.canUndo(): boolean
store.canRedo(): boolean
store.isDirty(): boolean
await store.save(options?): Promise<Uint8Array>
store.getSlideRevision(slideId: string): number

Events

store.on(event, handler) is the low-level channel the Root/Viewport callback props wrap. It returns an unsubscribe function.

store.on("slideChange", ({ slideId, index, reason }) => {});
store.on("statusChange", ({ status, previousStatus }) => {});
store.on("edit", ({ operation, source }) => {});
store.on("historyChange", ({ canUndo, canRedo, isDirty }) => {});
store.on("zoomChange", ({ zoom, previousZoom, reason }) => {});

Edit operations

All operations accept slideId and nodeId (both strings).

setTextRun

Replace a single run's text content. Preserves all other run formatting.

await store.edit({
  type: "setTextRun",
  slideId,
  nodeId,
  paragraphIndex: 0,
  runIndex: 0,
  text: "New text",
});

setTextBody

Replace all paragraphs and runs in a shape.

await store.edit({
  type: "setTextBody",
  slideId,
  nodeId,
  paragraphs: [
    {
      runs: [{ text: "Line one" }],
    },
    {
      runs: [{ text: "Line two" }],
    },
  ],
});

setNodeTransform

Move or resize a shape. All values are in slide pixels.

await store.edit({
  type: "setNodeTransform",
  slideId,
  nodeId,
  x: 100,
  y: 50,
  width: 400,
  height: 200,
  rotation: 0, // degrees, optional
});

setSolidFill

Change a shape's solid fill color.

await store.edit({
  type: "setSolidFill",
  slideId,
  nodeId,
  color: "#3b82f6", // hex
});

deleteNode

Remove a shape from a slide.

await store.edit({ type: "deleteNode", slideId, nodeId });

moveSlide

Reorder slides.

await store.edit({ type: "moveSlide", slideId, toIndex: 2 });

duplicateSlide

Clone a slide. The duplicate is inserted immediately after the source.

await store.edit({ type: "duplicateSlide", slideId });

deleteSlide

Remove a slide.

await store.edit({ type: "deleteSlide", slideId });

batch

Group multiple operations into a single undoable step.

await store.edit({
  type: "batch",
  ops: [
    { type: "deleteNode", slideId, nodeId: "2" },
    { type: "deleteNode", slideId, nodeId: "3" },
  ],
});

Hooks

import {
  useHistory, // { canUndo, canRedo, isDirty, undo, redo }
  usePresentation, // { presentation, status, progress, error }
  usePresentationStore, // the ambient store, for imperative calls
  useSlide, // { slide, slideId, index }
  useZoom, // { zoom, isAutoFit, setZoom, zoomIn, zoomOut, setAutoFit, fitTo }
} from "@diceui/pptx";

All hooks must be used inside Presentation.Root or Presentation.Provider.

useHistory

import { useHistory } from "@diceui/pptx";

function HistoryButtons() {
  const { canUndo, canRedo, isDirty, undo, redo } = useHistory();

  return (
    <>
      <button disabled={!canUndo} onClick={undo}>
        Undo
      </button>
      <button disabled={!canRedo} onClick={() => void redo()}>
        Redo
      </button>
      {isDirty && <span>Unsaved changes</span>}
    </>
  );
}

Don't wire this with store.subscribe. Undo history isn't in store state, so a save that clears isDirty wouldn't re-render. Outside React, use store.on("historyChange").

On this page