PowerPoint

Build a PPTX viewer or editor with Svelte 5. Open a file, add slide navigation, connect edits, and download the result.

Open a file

Read your PPTX file into a Uint8Array. The examples below accept these bytes as a component prop.

TypeScript
const bytes = new Uint8Array(await file.arrayBuffer());

Render a slide

Open the bytes with openPresentationArtifact and pass its document and a slide to PresentationSlideView. Give the viewer a fixed height so it can size the slide.

Svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { openPresentationArtifact } from '@tumblerjs/slides';
  import { PresentationSlideView } from '@tumblerjs/svelte/slides';

  let { bytes }: { bytes: Uint8Array } = $props();
  const artifact = untrack(() => openPresentationArtifact(bytes));
  let scale = $state(1);
</script>

<div style="height: 540px; min-width: 0">
  {#if artifact.document.slides[0]}
    <PresentationSlideView
      presentation={artifact.document}
      slide={artifact.document.slides[0]}
      bind:scale
    />
  {/if}
</div>

A scale of 1 fits the slide to the viewer. Bind scale to keep zoom controls in sync with pinch gestures. These examples open one file per component instance; remount the component when loading another file.

Bind PresentationSlideRail to an index and use that index for the main view. Handle onslide so links inside the presentation can switch slides too.

Svelte
<script lang="ts">
  import type { PresentationDocument } from '@tumblerjs/slides';
  import { PresentationSlideRail, PresentationSlideView } from '@tumblerjs/svelte/slides';

  let { presentation }: { presentation: PresentationDocument } = $props();
  let index = $state(0);
</script>

<div style="display: flex; height: 540px; min-width: 0">
  <aside style="width: 150px; flex-shrink: 0">
    <PresentationSlideRail {presentation} bind:index />
  </aside>
  <div style="flex: 1; min-width: 0">
    {#if presentation.slides[index]}
      <PresentationSlideView
        {presentation}
        slide={presentation.slides[index]!}
        onslide={(part) => {
          const next = presentation.slides.findIndex(slide => slide.part === part);
          if (next >= 0) index = next;
        }}
      />
    {/if}
  </div>
</div>

Connect editing

Use an editing session instead of a read-only artifact. Set editable on the view and connect its callbacks to the session. Reassign the returned artifact after each edit so the view updates.

Svelte
<script lang="ts">
  import { untrack } from 'svelte';
  import { openPresentationEditingSession, type PresentationArtifact } from '@tumblerjs/slides';
  import { PresentationSlideView } from '@tumblerjs/svelte/slides';

  let { bytes }: { bytes: Uint8Array } = $props();
  const session = untrack(() => openPresentationEditingSession(bytes));
  let artifact = $state(session.artifact);
  let index = $state(0);
  let canUndo = $state(false);
  let canRedo = $state(false);
  let error = $state('');

  function apply(operation: () => PresentationArtifact) {
    try {
      artifact = operation();
      canUndo = session.canUndo;
      canRedo = session.canRedo;
      error = '';
    } catch (cause) {
      error = cause instanceof Error ? cause.message : 'Could not apply this edit.';
    }
  }
</script>

<button disabled={!canUndo} onclick={() => apply(() => session.undo())}>Undo</button>
<button disabled={!canRedo} onclick={() => apply(() => session.redo())}>Redo</button>
{#if error}<p role="alert">{error}</p>{/if}

<div style="height: 540px; min-width: 0">
  {#if artifact.document.slides[index]}
    <PresentationSlideView
      presentation={artifact.document}
      slide={artifact.document.slides[index]!}
      editable
      onobjectchange={(change) => apply(() => session.updateObject(change))}
      ontextedit={(change) => apply(() => session.editText(change))}
      onformat={(change) => apply(() => session.formatText(change))}
      onshapechange={(change) => apply(() => session.styleShape(change))}
      onundo={(redo) => apply(() => redo ? session.redo() : session.undo())}
    />
  {/if}
</div>

The text and shape formatting controls appear when their callbacks are connected. To add the thumbnail rail, pass artifact.document as its presentation and bind the same index used by the view.

Use the editor

ActionControl
Move an objectDrag it, or select it and use the arrow keys.
Resize or rotateDrag an edge or corner handle to resize. Use the handle above the object to rotate; hold Shift to snap to 15° increments.
Edit textDouble-click a text box, or select it and press Enter. Select words to apply formatting with the toolbar.
Edit a tableDouble-click a cell. Tab and Shift+Tab move between cells.
Finish editingPress Escape or Ctrl+Enter. Click blank slide space to clear the selection.
Change shape coloursSelect the shape, then use the fill and outline controls.

Download the edited file

Add this function to the editor script and call it from your Download button. Call bytes() when saving, not after every edit: it builds the PPTX archive on demand.

TypeScript
function download() {
  const bytes = session.artifact.bytes();
  const blob = new Blob([Uint8Array.from(bytes).buffer], {
    type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
  });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = 'presentation.pptx';
  link.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
Svelte
<button onclick={download}>Download</button>

For server storage, send the returned Uint8Array through your upload flow instead. Edits remain in memory until you save them.

PowerPoint format support and limitations

Next Try the editor with an example deck