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

@office-kit/pptx

v0.24.1

Published

Generate and edit .pptx (OOXML PresentationML) files from TypeScript, in Node and the browser.

Readme

@office-kit/pptx

npm CI license

Read, edit, and write .pptx (Office Open XML Presentation) files from TypeScript, in Node.js and the browser, from a single ESM bundle.

Create slides with AI · Documentation · Playground (inspect a deck in your browser) · REPL (write code, watch the deck redraw)

import { loadPresentation, replaceTokensInPresentation, savePresentation } from '@office-kit/pptx';

// A designer makes template.pptx in a presentation app. Your code fills it in.
const pres = await loadPresentation(templateBytes);
replaceTokensInPresentation(pres, { name: 'Alice', event: 'Re:Invent', date: '2026-12-01' });
const out: Uint8Array = await savePresentation(pres);

Status: 0.x, pre-1.0. The capabilities in the scope table are exercised against real PPTX fixtures and validated in CI (see How output is checked). Until 1.0 the public API is not frozen: a breaking change can land in a minor (0.x) release, so pin a version or an exact range.

Create slides with AI and live preview

Create presentations in TSX with a live browser preview and Claude Code or Codex beside the slide. Select an area and ask the agent to change it, edit text directly, and export an editable .pptx file. Changes are saved in the TSX source; saving a source file also updates the preview.

Requires Node.js 22.18 or later. Create a project and start the preview in one command (macOS/Linux):

npx --yes @office-kit/pptx-dev@latest init my-slides && cd my-slides && npm install && npm run dev

Open the local URL printed by the server. The generated project includes deck.tsx, individual slides, a shared theme and a CLAUDE.md authoring guide. For later sessions, run this inside the project:

npx office-pptx dev deck.tsx

npm run dev starts the same preview. To update an existing project's dev tools, stop the server, then run:

npm install -D @office-kit/pptx-dev@latest
npm run dev

Run npm run check to type-check the deck and npm run build to export deck.pptx. AI editing uses your locally installed and authenticated Claude Code or Codex CLI. After edits, changed-slide screenshots are supplied to the agent for design review; capture requires Chrome or Playwright Chromium.

See the preview and agent setup, selection, text editing and visual review, and TSX element reference. To let Claude Code handle project setup as well, follow the skill installation guide.

Why this library

  • It reads as well as it writes. Open a deck made in Keynote, Google Slides or any other presentation app, change it, and save it. Every setter has a getter, and there are deck-wide queries (find every hyperlink, every comment by an author, every slide with an empty title).
  • Parts it does not model survive the round trip. SmartArt, OLE objects, video, modern threaded comments, and vendor extensions are carried through untouched. The library never silently strips what it does not understand.
  • The output is valid, not "valid enough". The Open XML SDK's OpenXmlValidator gates every CI run. A file that opens in one presentation app but breaks Keynote is treated as a bug.
  • One ESM bundle for Node and the browser. No fs, Buffer, or zlib on the hot path, and one runtime dependency (fflate, for ZIP).
  • You ship only what you import. The API uses side-effect-free functions. CI checks tree-shaking and bundle-size limits (test/tree-shake.test.ts).
  • Types follow the spec. The model mirrors ECMA-376 Part 1 §19 (PresentationML). Positions are branded Emu numbers, so inches and points cannot be mixed up by accident.

How it differs from PptxGenJS

PptxGenJS is the established way to generate a deck in JavaScript, and it is good at that job. The difference is direction: PptxGenJS writes new files; @office-kit/pptx reads, edits, and writes. If your deck starts from a template, or from a file a person made, PptxGenJS cannot open it.

Compared against PptxGenJS 4.0.1:

| | @office-kit/pptx | PptxGenJS | | --------------------------------- | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | Open and edit an existing .pptx | ✅ Load, change, save; unknown parts are preserved | ❌ Creates new files only | | Templates | Any .pptx a designer made in a presentation app | Slide masters defined in code (defineSlideMaster) | | Read back what is in a deck | ✅ Every setter has a getter, plus deck-wide queries | ❌ The API is write-only | | API shape | Tree-shakeable functions (addSlideChart(slide, …)) | One class with methods (slide.addChart(…)) | | Module formats | ESM only | ESM, CommonJS, and a script-tag bundle | | Runtime dependencies | 1 (fflate) | 4 (jszip, image-size, https, @types/node) | | Slide transitions | ✅ | ❌ | | Animations | ✅ All 95 gallery presets | ❌ | | Comments | ✅ | ❌ (speaker notes only) | | Chart types you can author | ✅ All 16 ECMA-376 plot types, incl. 3-D, stock, surface, pie-of-pie; combos, error bars, data table, date axis | Bar, line, area, pie, doughnut, scatter, bubble, radar, 3-D bar / bubble; combos | | Audio, video, YouTube embeds | ✅ Embedded from bytes (Node and browser), with read-back (getShapeMedia); clips de-duplicated per deck | ✅ From a path or base64; write-only | | HTML <table> to slides | ❌ | ✅ With automatic paging | | Render a slide to an image | ✅ SVG and PNG, via @office-kit/pptx-preview | ❌ | | How output is checked | Open XML SDK validator and ECMA-376 XSDs, in CI | Manual runs in presentation apps before a release |

Pick PptxGenJS if you only ever generate new decks and need HTML-table import, CommonJS, or a <script>-tag build.

Pick @office-kit/pptx if a template, an existing deck, or a validation requirement is involved, or if you need to read a deck as well as write one.

The two also work together. Decks written by PptxGenJS are part of this library's test fixtures (test/pptxgenjs-compat.test.ts), so you can generate with PptxGenJS and post-process the result here.

The same slide with PptxGenJS, the core API, and TSX

// PptxGenJS
import pptxgen from 'pptxgenjs';

const pptx = new pptxgen();
pptx.layout = 'LAYOUT_WIDE';
const slide = pptx.addSlide();
slide.addText('Q3 Review', { x: 1, y: 0.5, w: 8, h: 1, fontSize: 28, bold: true });
slide.addChart(
  pptx.ChartType.bar,
  [{ name: 'Revenue', labels: ['Q1', 'Q2'], values: [120, 180] }],
  {
    x: 1,
    y: 1.5,
    w: 8,
    h: 4,
    barDir: 'col',
  },
);
await pptx.writeFile({ fileName: 'out.pptx' });
// @office-kit/pptx: the Node barrel includes the core API and file helpers.
import * as pptx from '@office-kit/pptx/node';

const { inches } = pptx;
const pres = pptx.createPresentation();
const slide = pptx.addBlankSlide(pres);
const title = pptx.addSlideTextBox(slide, {
  x: inches(1),
  y: inches(0.5),
  w: inches(8),
  h: inches(1),
  text: 'Q3 Review',
});
pptx.setShapeTextFormat(title, { size: 28, bold: true });
pptx.addSlideChart(slide, {
  x: inches(1),
  y: inches(1.5),
  w: inches(8),
  h: inches(4),
  spec: {
    kind: 'column',
    categories: ['Q1', 'Q2'],
    series: [{ name: 'Revenue', values: [120, 180] }],
  },
});
await pptx.savePresentationToFile(pres, 'out.pptx');
// @office-kit/pptx-dsl — deck.tsx
import { Presentation, Slide, Text, Chart } from '@office-kit/pptx-dsl';

export default (
  <Presentation>
    <Slide>
      <Text x={1} y={0.5} width={8} height={1} size={28} bold>
        Q3 Review
      </Text>
      <Chart
        x={1}
        y={1.5}
        width={8}
        height={4}
        spec={{
          kind: 'column',
          categories: ['Q1', 'Q2'],
          series: [{ name: 'Revenue', values: [120, 180] }],
        }}
      />
    </Slide>
  </Presentation>
);

Save the TSX example as deck.tsx in an initialized authoring project, then run npx --no-install office-pptx build deck.tsx --out out.pptx.

The core API uses explicit units (inches(1), cm(2.5), pt(12)). The DSL uses inches for numeric geometry and points for font sizes, and compiles to the same core model. Both produce editable native text and charts.

How output is checked

  • Open XML SDK. A CI job generates the sample decks and runs the Open XML SDK's OpenXmlValidator over them (tools/ooxml-validate). Any validation error fails the build.
  • ECMA-376 XSDs. Emitted XML is validated against the official schemas with xmllint in the test suite (these tests skip on a machine without xmllint).
  • Real files and a parity corpus. Fixture tests round-trip decks written by python-pptx and PptxGenJS, and test/corpus authors the same slide once with PptxGenJS and once with this library, then diffs the two drawing trees. The suite runs on Node 22, 24, and 26.
  • At runtime. validatePresentation(pres) checks package invariants (missing relationships, dangling slide ids, duplicate shape ids, layouts without masters) in Node and the browser.

Scope

The work is split into four levels. The 0.x line covers levels 1–3 and part of level 4. Items marked "post-1.0" are not implemented yet:

| Level | Capability | 0.x | | ----- | ------------------------------------------------------------------- | ------------------------------- | | L1 | Read an existing PPTX, save it back without corruption | ✅ | | L2 | Template edit: text replacement, image swap, add slide from layout | ✅ | | L3 | Authoring: shapes, text, tables, fills, effects, transforms, groups | ✅ | | L3 | Authoring on top of existing themes / masters / layouts | ✅ | | L3 | Rebranding a deck: theme colors and theme fonts | ✅ | | L3 | Constructing new themes / masters / layouts from scratch | ❌ post-1.0 | | L3 | Charts: all 16 plot types (incl. 3-D, stock, surface), and combos | ✅ | | L4 | Notes, comments, transitions | ✅ | | L4 | Simple animations (entrance / exit presets) | ✅ | | L4 | Audio / video / online video: embed, link and read back | ✅ | | L4 | SmartArt authoring | ❌ post-1.0 (read pass-through) | | L4 | Complex animation timing trees | ❌ post-1.0 | | L4 | OLE / ActiveX authoring | ❌ post-1.0 (read pass-through) | | L4 | Document encryption (read + write) | ❌ post-1.0 |

Out-of-scope content is still preserved on round-trip. That is the L1 contract.

When NOT to use this:

  • You need a pixel-perfect PPTX rendering (print, archival). The companion @office-kit/pptx-preview package renders slides to SVG in the browser and to PNG on the server, and its closeness to LibreOffice is measured per slide and gated in CI (site/fidelity). It is a high-fidelity preview, not a spec-complete paint engine. For pixel-authoritative output, use a desktop presentation app itself or LibreOffice headless.
  • You only generate new decks and need a feature in the PptxGenJS column above. Use PptxGenJS.
  • You want to convert PPTX to another format (Keynote, ODP). Out of scope forever; that is a renderer's job.

Install

npm install @office-kit/pptx
# or
pnpm add @office-kit/pptx
# or
yarn add @office-kit/pptx

Packages

| Package | What it does | | ---------------------------------------------- | ------------------------------------------------------------------------------ | | @office-kit/pptx (this directory) | Read, edit and write .pptx files. Node and browser. | | @office-kit/pptx-preview | Render a slide to SVG (browser and Node) or PNG (Node). | | @office-kit/pptx-dsl | Write a presentation as typed TSX. | | @office-kit/pptx-editor | Embed a desktop-style slide editor in your web application with mountEditor. | | @office-kit/pptx-dev | Preview, edit and export a TSX presentation locally. |

One API

@office-kit/pptx exposes a single tree-shakeable free-function API. Every capability is a named export — loadPresentation, savePresentation, addSlideTextBox, setShapeFill, etc. Bundlers drop every entry you don't import, so the minimal load → save bundle is about 56 KB unminified. CI enforces the bound in test/tree-shake.test.ts.

import {
  findSlidePlaceholder,
  getSlides,
  loadPresentation,
  savePresentation,
  setShapeText,
} from '@office-kit/pptx';

const pres = await loadPresentation(bytes);
const title = findSlidePlaceholder(getSlides(pres)[0]!, 'title');
if (title) setShapeText(title, 'Hello');
const out = await savePresentation(pres);

Driving @office-kit/pptx from an AI agent

Install the Claude Code plugin once from the shared Office Kit marketplace:

claude plugin marketplace add office-kit/skills
claude plugin install pptx@office-kit

In Claude Code, open /plugin → Marketplaces → office-kit and enable auto-update once. Third-party marketplaces do not enable it by default. Updates load after /reload-plugins or a new session.

Then ask Claude Code:

/pptx:office-kit-pptx Create a quarterly business review with a revenue chart
and a next-actions table. Use labelled sample data where needed.

The skill creates a TSX project, starts the interactive preview, checks the source and exports an editable PPTX. Ask for changes in the same conversation; the preview updates as the agent edits TSX. It plans the storyline for either a talk or a deck people read and decide from, and checks the headlines and Japanese wording before delivery (talk, document, review). You need Node.js 22.18+ and Git alongside Claude Code. See the authoring guide for template editing and manual setup. The bundled core reference and tested example cover direct API use.

Usage

Edit a template

import {
  findSlidePlaceholder,
  getSlides,
  loadPresentation,
  savePresentation,
  setShapeText,
} from '@office-kit/pptx';

const pres = await loadPresentation(existingPptxBytes);
const cover = getSlides(pres)[0]!;
const title = findSlidePlaceholder(cover, 'title');
if (title) setShapeText(title, 'Q3 Review');
const body = findSlidePlaceholder(cover, 'body');
if (body) setShapeText(body, 'Numbers up and to the right.');

const out: Uint8Array = await savePresentation(pres);
// Node:    fs.writeFile('out.pptx', out)
// Browser: new Blob([out], { type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' })

Token-based template fill

import { loadPresentation, replaceTokensInPresentation, savePresentation } from '@office-kit/pptx';

const pres = await loadPresentation(templateBytes);
// Replaces `{{name}}`, `{{event}}`, `{{date}}` across every slide.
replaceTokensInPresentation(pres, { name: 'Alice', event: 'Re:Invent', date: '2026-12-01' });
const out = await savePresentation(pres);

Build a deck from scratch (no template file)

createPresentation() returns an immediately-authorable deck — a slide master, a default theme, and three layouts (Blank, Title Slide, Title and Content) — with no slides yet. No .pptx template needed.

import {
  addContentSlide,
  addTitleSlide,
  createPresentation,
  findSlideLayoutByType,
  addSlide,
  findSlidePlaceholder,
  savePresentation,
  setShapeText,
} from '@office-kit/pptx';

// Defaults to 16:9; pass { size: '4:3' } for the classic ratio.
const pres = createPresentation();

// Sugar helpers pick the right layout by its locale-stable type token.
addTitleSlide(pres, 'Q3 Business Review');
addContentSlide(pres, { title: 'Agenda', body: 'Highlights and risks' });

// Or bind a layout explicitly. Prefer findSlideLayoutByType — it matches
// the `type` token (`'title'`, `'obj'`, `'blank'`), which is stable
// across the reference desktop app's UI languages. findSlideLayout(pres, 'Blank') matches
// the user-visible name, which is case-sensitive and localized.
const titleLayout = findSlideLayoutByType(pres, 'title')!;
const slide = addSlide(pres, { layout: titleLayout });
setShapeText(findSlidePlaceholder(slide, 'ctrTitle')!, 'Authored with @office-kit/pptx');

const out: Uint8Array = await savePresentation(pres);

Add slides to your own template

import {
  addSlide,
  addSlideImage,
  addSlideTextBox,
  duplicateSlide,
  findSlideLayout,
  findSlidePlaceholder,
  inches,
  loadPresentation,
  moveSlide,
  savePresentation,
  setShapeText,
} from '@office-kit/pptx';

// Layout names come from the template; list them with getSlideLayouts(pres).
const pres = await loadPresentation(await fetch('/template.pptx').then((r) => r.arrayBuffer()));

const titleLayout = findSlideLayout(pres, 'Title Slide')!;
const slide1 = addSlide(pres, { layout: titleLayout });
setShapeText(findSlidePlaceholder(slide1, 'ctrTitle')!, '@office-kit/pptx demo');
setShapeText(findSlidePlaceholder(slide1, 'subTitle')!, 'an OOXML library for TypeScript');

const blank = findSlideLayout(pres, 'Blank')!;
const slide2 = addSlide(pres, { layout: blank });
addSlideTextBox(slide2, {
  x: inches(1),
  y: inches(1),
  w: inches(8),
  h: inches(1),
  text: 'Free-form text box',
});
addSlideImage(slide2, imageBytes, { x: inches(1), y: inches(3), w: inches(3), h: inches(3) });

const dup = duplicateSlide(pres, slide2);
moveSlide(pres, dup, 0);

const out: Uint8Array = await savePresentation(pres);

Add video, audio or an online video

import { addSlideMedia, getShapeMedia, inches } from '@office-kit/pptx';

const box = { x: inches(1), y: inches(1.5), w: inches(8), h: inches(4.5) };

// Embedded video / audio, from bytes. The container is detected from the bytes.
const clip = addSlideMedia(slide, {
  kind: 'video',
  data: mp4Bytes,
  poster: posterPngBytes,
  ...box,
});
addSlideMedia(slide, {
  kind: 'audio',
  data: mp3Bytes,
  x: inches(0.5),
  y: inches(6.5),
  w: inches(0.6),
  h: inches(0.6),
});

// Online video. YouTube watch / youtu.be / shorts URLs become the embed URL.
addSlideMedia(slide2, {
  kind: 'online',
  url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
  ...box,
});

const media = getShapeMedia(clip);
// { kind: 'video', partName: '/ppt/media/media1.mp4', contentType: 'video/mp4', bytes }
// an online video reads back as { kind: 'online', url }

The shape is an ordinary picture showing the poster frame, so setShapeImage replaces the poster and getShapeImageBytes reads it. Without poster a neutral play-button image is used. Containers detected from the bytes: mp4, m4v, mov, webm, avi, wmv, mp3, wav, m4a, ogg, wma — pass format when the signature check cannot tell which of those a file is. Identical clip bytes are stored once per deck, and the clip survives copyShape, duplicateSlide and importSlide. Whether a clip plays depends on the codecs of the application showing the deck.

Replace an image in place

import {
  getShapeKind,
  getShapeName,
  getSlideShapes,
  getSlides,
  loadPresentation,
  savePresentation,
  setShapeImage,
} from '@office-kit/pptx';

const pres = await loadPresentation(templateBytes);
for (const slide of getSlides(pres)) {
  for (const shape of getSlideShapes(slide)) {
    if (getShapeKind(shape) === 'picture' && getShapeName(shape) === 'Logo') {
      setShapeImage(shape, newLogoBytes); // format auto-detected; geometry preserved
    }
  }
}
const out = await savePresentation(pres);

Node convenience entry

import { loadPresentationFile, savePresentationToFile } from '@office-kit/pptx/node';

const pres = await loadPresentationFile('./template.pptx');
await savePresentationToFile(pres, './out.pptx');

Charts

import {
  addSlideChart,
  getSlides,
  loadPresentation,
  savePresentation,
  inches,
} from '@office-kit/pptx';

const pres = await loadPresentation(templateBytes);
const slide = getSlides(pres)[0];
addSlideChart(slide!, {
  x: inches(0.5),
  y: inches(0.5),
  w: inches(8),
  h: inches(4.5),
  spec: {
    kind: 'column', // bar | column | line | pie | doughnut | area (combine per series with `chartKind`)
    categories: ['Q1', 'Q2', 'Q3', 'Q4'],
    series: [
      { name: 'Revenue', values: [120, 180, 240, 300] },
      { name: 'Cost', values: [80, 90, 130, 160] },
    ],
    title: 'FY26 plan',
  },
});

await savePresentation(pres);

The embedded xlsx that the reference desktop app requires for "Edit data" is generated automatically. Inline <c:strCache> / <c:numCache> caches mean the chart renders without opening the workbook.

Animations

import { setShapeAnimation, getSlideShapes, getSlides } from '@office-kit/pptx';

const slide = getSlides(pres)[0]!;
const shape = getSlideShapes(slide)[0]!;
setShapeAnimation(shape, { effect: 'fadeIn', durationMs: 800 });
setShapeAnimation(shape, { effect: 'flyIn', direction: 'topLeft', start: 'afterPrevious' });
setShapeAnimation(shape, { effect: 'shapeOut', shape: 'diamond', inOut: 'out' });

All 95 presets of the reference desktop app's Entrance (35), Emphasis (24) and Exit (36) galleries are written exactly as the reference desktop app writes them: the same preset numbers, behaviours, default duration and build entry. Entrances end in In and exits in Out (flyIn / flyOut, basicZoomIn, growTurnIn / shrinkTurnOut, creditsIn, …), except appear / disappear; emphasis effects have plain names (spin, pulse, darken, fillColor, wave, …). zoomIn / zoomOut are the Subtle gallery's Zoom (preset 53), and basicZoomIn / basicZoomOut are Basic Zoom (preset 23).

Options are the ones the reference desktop app's Effect Options offer: direction for fly… (eight), wipe… / peek… (four edges) and strips… (four corners); orientation for blinds…, checkerboard…, randomBars…; orientation and inOut for split…; shape and inOut for shape…; spokes for wheel…; spinDirection and spinDegrees for spin; scaleDirection and scalePercent for growShrink; transparencyPercent for transparency; and color (a theme slot or #RRGGBB, optionally with colour transforms) for the colour emphasis effects. getSlideAnimations reads every option back. durationMs rescales every behaviour of the effect together, the way the reference desktop app's Duration box does; updateSlideAnimation with a new effect gives it that preset's default duration unless the patch states one. build is the Sequence option for text: 'asOneObject' (the default), 'allAtOnce' or 'byParagraph'.

Transitions

import { getSlides, setSlideTransition } from '@office-kit/pptx';

const slide = getSlides(pres)[0]!;
setSlideTransition(slide, { effect: 'push', direction: 'u', durationMs: 1000 });
setSlideTransition(slide, { effect: 'vortex', direction: 'r' }); // a 2010+ extension effect
setSlideTransition(slide, { effect: 'prstTrans', preset: 'curtains' });
setSlideTransition(slide, { effect: 'morph', morphOption: 'byWord' });

The ECMA-376 effects are written as <p:…> elements. The 2010+ extension effects (vortex, ripple, honeycomb, prism, doors, window, ferris, gallery, conveyor, pan, glitter, warp, flythrough, flash, shred, reveal, switch, flip, wheelReverse), the prstTrans presets and morph are written the way the reference desktop app writes them: inside mc:AlternateContent, with a <p:fade/> fallback for readers that do not know them. Their options are pattern, isContent, isInverted, hasBounce, preset, invertX, invertY and morphOption.

Comments

import { addSlideComment, getSlides } from '@office-kit/pptx';

const slide = getSlides(pres)[0]!;
addSlideComment(slide, {
  author: { name: 'Reviewer A' },
  text: 'Punch up the numbers here.',
  position: { x: 1_000_000, y: 1_000_000 }, // optional EMU coords
});

Gradient fills

import { setShapeGradientFill } from '@office-kit/pptx';

setShapeGradientFill(shape, {
  stops: [
    { offset: 0, color: '#FF0000' },
    { offset: 1, color: '#0000FF' },
  ],
  angleDeg: 90, // top → bottom
});

Validation

import { validatePresentation } from '@office-kit/pptx';

const issues = validatePresentation(pres);
for (const i of issues) console.error(i.severity, i.message);
// Catches missing rels, dangling slide ids, layouts without masters, etc.

Copy formatted text between shapes

setShapeParagraphs accepts either paragraph specifications or an existing text shape. Copying from a shape retains paragraph properties, run formatting, complete fields and hyperlink relationships, including across presentations:

import { setShapeParagraphs } from '@office-kit/pptx';

setShapeParagraphs(destination, { source });
setShapeParagraphs(destination, { source, range: { start: 6, end: 12 } });
// Append whole paragraphs without flattening formatting, fields or hyperlinks.
setShapeParagraphs(destination, { sources: [destination, source] });
// Reorder ranges into one text body, retaining their paragraph formatting.
setShapeParagraphs(destination, {
  source,
  ranges: [
    { start: 6, end: 12 },
    { start: 0, end: 5 },
  ],
});
setShapeParagraphs([first, second], {
  source,
  ranges: [
    { start: 0, end: 5 },
    { start: 6, end: 12 },
  ],
});

Ranges use UTF-16 offsets with an exclusive end, including one character per paragraph separator. A partially selected field becomes literal text. The source stays unchanged unless it is also a destination; each destination retains its text-body settings and list styles. All batch ranges are read before any destination text changes. getShapeParagraphElements(shape) reads all paragraphs in one pass; passing an index reads one paragraph.

To create several destination slides, use addSlideAt(pres, index, [{ layout }, { layout }]). It returns the new slides in insertion order, with existing slide handles preserved. Every layout must belong to the presentation.

API surface (current state)

Each row lists the free-function entry points. Read/write pairs are shown together.

| Capability | API | | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Load / save | loadPresentation(input), savePresentation(pres), loadPresentationFile(path) (node), savePresentationToFile(pres, path) (node) | | Create | createPresentation({ size?: '16:9' \| '4:3' }) — blank deck with master + theme + Blank / Title Slide / Title and Content layouts | | Slide CRUD | getSlides, getSlideAt, getSlideIndex, addSlide, removeSlide, moveSlide, duplicateSlide, clearSlideShapes | | Slide layout | getSlideLayouts, findSlideLayout (by name — case-sensitive, exact; pass a RegExp for case-insensitive), findSlideLayoutByType (by locale-stable type token — preferred), getSlideLayout(slide), setSlideLayout(slide, layout), getSlideLayoutName, getSlideLayoutType | | Slide masters | getSlideMasterPartNames, getSlideMasterLayouts, getSlideMasterPlaceholders, getSlideMasterName / setSlideMasterName, isSlideMasterPreserved / setSlideMasterPreserved, addSlideMaster / removeSlideMaster, addSlideLayout / removeSlideLayout, setSlideMasterPlaceholderIncluded, getSlideLayoutPlaceholders, addSlideLayoutPlaceholder, setSlideLayoutTitleIncluded, setSlideLayoutFootersIncluded, setSlideLayoutBackgroundGraphicsHidden — removal refuses masters and layouts that slides use | | Notes and handout masters | getNotesMasterPlaceholders / setNotesMasterPlaceholderIncluded, getHandoutMasterPlaceholders / setHandoutMasterPlaceholderIncluded (the first edit writes the reference desktop app's default master), getNotesPageSize / setNotesPageOrientation, getHandoutSlidesPerPage / setHandoutSlidesPerPage | | Slide metadata | getSlideTitle / setSlideTitle, getSlideSize / setSlideSize, isSlideHidden / setSlideHidden, getSlideText | | Slide sections | getSlideSections, setSlideSections (p14 sectionLst) | | Placeholders | findSlidePlaceholder(slide, 'title' \| 'body' \| ...) | | addSlidePlaceholder(slide, type, { source?: 'layout' \| 'master' }) | Restore one missing layout placeholder, or explicitly inherit from the master without changing layouts. | | Token / text replace | replaceTokensInPresentation, replaceTokensInSlide, replaceTextInPresentation, replaceTextInSlide | | Background | getSlideBackground / setSlideBackground / clearSlideBackground | | Notes | getSlideNotes / setSlideNotes | | Transitions | getSlideTransition / setSlideTransition / clearSlideTransition | | Animations | getShapeAnimation / setShapeAnimation (entrance / exit / emphasis presets), clearSlideAnimations | | Comments | addSlideComment, getSlideComments, removeSlideComment, getCommentAuthors, getCommentText / getCommentAuthor / getCommentPosition | | Shape authoring | addSlideTextBox, addSlideShape, addSlideLine, addSlideTable, addSlideImage, addSlideMedia, addSlideChart | | Shape lookup | findShapeByName, findShapesByName, findShapesByKind, findShapeInPresentation, getAllShapes, getSlideShapes | | Shape text | setShapeText, setShapeParagraphs, setShapeBulletStyle, setShapeAlignment, setShapeTextFormat, setShapeHyperlink / getShapeHyperlink | | Per-paragraph | setParagraphAlignment / getParagraphAlignment, setParagraphLevel / getParagraphLevel, setParagraphBullet / getParagraphBullet, setParagraphSpacing / getParagraphSpacing, setParagraphLineSpacing / getParagraphLineSpacing, getParagraphEndFormat | | Per-run text | setShapeRunText / getShapeRunText, setShapeRunFormat / getShapeRunFormat, getShapeParagraphCount, getShapeRunCount | | Text frame | setShapeTextAnchor / getShapeTextAnchor, setShapeTextMargins / getShapeTextMargins | | Fill | setShapeFill / getShapeFill, setShapeGradientFill, setShapePatternFill, setShapeImageFill, setShapeNoFill, clearShapeFill | | Stroke | setShapeStroke / getShapeStroke (solid color or gradient fill; getShapeStrokeGradient), setShapeStrokeDash / getShapeStrokeDash, setShapeStrokeArrow / getShapeStrokeArrow, setShapeStrokeSketch / getShapeStrokeSketch (sketched lines: curved / freehand / scribble), …NoStroke | | Effects | setShapeShadow / setShapeGlow / getShapeEffect, clearShapeEffects | | Geometry | setShapePosition, setShapeSize, setShapeRotation, setShapeFlip, setShapeBounds / getShapeBounds | | Pictures | setShapeImage, setShapeImageCrop / getShapeImageCrop, setShapeImageOpacity / getShapeImageOpacity, setShapeImageBrightness, …Contrast, getShapeImageArtisticEffect, setShapePictureStyle / getShapePictureStyle (the reference desktop app's 28 built-in picture styles, BUILTIN_PICTURE_STYLES), setShapeImageCompressionState / getShapeImageCompressionState | | Z-order | bringShapeToFront, sendShapeToBack, bringShapeForward, sendShapeBackward, getShapeZIndex, setShapeZIndex | | Click actions | setShapeClickAction / getShapeClickAction (url / slide / nextSlide / prevSlide / firstSlide / lastSlide) | | Shape removal | removeShape | | Tables | getTableCell / getTableCells, setTableCellText / getTableCellText, setTableCellParagraphs / getTableCellParagraphs, setTableCellFill / clearTableCellFill, setTableCellAlignment, setTableCellTextFormat, insertTableRow / removeTableRow, insertTableColumn / removeTableColumn, mergeTableCells / getTableCellSpan | | Table styles | setTableStyleId (a GUID or a built-in name from BUILTIN_TABLE_STYLES; writes the reference desktop app's definition to tableStyles.xml) / getTableStyleId, setTableStyleFlags / getTableStyleFlags, getTableCellAppearanceEffective, getTableBackgroundEffective | | Charts | addSlideChart, getSlideCharts, setChartSpec — kinds: bar, column, line, pie, doughnut, area; axis tick labels via categoryAxisTickLabelPos / valueAxisTickLabelPos / secondaryValueAxis.tickLabelPos; axis line / gridline widths via valueAxisLineWidthEmu, valueAxisMajorGridlineWidthEmu and their category / secondary-axis mirrors; series lineColor / markerColor / markerLineColor; dataLabels.showLeaderLines | | Theme | getPresentationTheme / setPresentationTheme — color scheme (accent1..accent6, dark1, light1, hyperlink, ...); getPresentationFonts / setPresentationFonts — major / minor Latin, East Asian, and complex-script faces | | Groups | groupShapes, ungroupShapes, getGroupChildren, getGroupTransform | | Text autofit | setShapeTextAutoFit / getShapeTextAutoFit, getShapeTextAutoFitParams | | Text 3-D (text-art bevels) | setShapeText3D / getShapeText3D (getTableCellText3D for cells) — <a:scene3d> camera and light rig, <a:sp3d> top bevel, extrusion, material, contour color; setShapeTextFlat / getShapeTextFlat (Keep text flat) | | Preset adjustments | setShapeAdjustValues; getShapeCustomGeometry (custom geometry is read-only) | | Document properties | getCoreProperties / setCoreProperties, touchModified, getThumbnail / setThumbnail / removeThumbnail | | Package inspection | listPackageParts, readPackagePart, getMediaParts, getOrphanMediaPartNames, getPackageSize, compactPackage | | Validation | validatePresentation(pres) — invariant checks, returns ValidationIssue[] | | Units | inches(n), cm(n), mm(n), pt(n), emu(n) — return branded Emu numbers |

setParagraphLevel also accepts a UTF-16 text range and a relative level change, so a selection can be indented in one update without rebuilding its text or links:

setParagraphLevel(shape, { start: 0, end: 24 }, { offset: 1 });
const levels = getParagraphLevel(shape, { start: 0, end: 24 });

The end offset is exclusive; a collapsed range selects the caret's paragraph. Relative levels clamp to 0–8. Passing a paragraph index and an absolute level continues to edit that single paragraph.

Outline view state

getCollapsedOutlineSlides(presentation) returns collapsed slides in deck order. Use setSlideOutlineCollapsed(slide, true) to hide a slide's body in the reference desktop app's outline view, or false to expand it. The setting survives a save/load round trip without changing the slide's text. Pass an array of slides to update a selection or the whole deck in one operation:

setSlideOutlineCollapsed(getSlides(presentation)[0]!, true);
const collapsedSlides = getCollapsedOutlineSlides(presentation);
setSlideOutlineCollapsed(getSlides(presentation), false);

Ordered shape batches

setShapeZIndex(shape, index) moves one object among its siblings. Pass an array instead to insert a contiguous batch in the supplied back-to-front order: setShapeZIndex([third, first], 0). Shapes must share a parent container; duplicate handles and mixed containers are rejected before mutation. Non-shape XML and unselected siblings are preserved.

Text formats (TextFormat, including a paragraph's endFormat) cover the Latin, East Asian and complex-script typefaces (font, fontEastAsian, fontComplexScript — <a:latin>, <a:ea>, <a:cs>). Each is authored and read on its own; setting one leaves the others as they were. Chart labels carry the Latin / East Asian pair and the complex-script slot on ChartTextStyle: font fills <a:latin> and <a:ea>, fontComplexScript fills <a:cs>, and neither implies the other. Rebuilding a typeface writes the typeface attribute only, so any pitchFamily / charset the source file carried on that element is dropped.

A run's colors take the reference desktop app's theme tints the way gradient stops do: colorTransforms beside color, and inside outline, shadow, innerShadow and glow, writes <a:lumMod>, <a:lumOff>, <a:tint>, ... as children of that color, so { color: 'accent2', colorTransforms: [{ kind: 'lumMod', value: 0.4 }, { kind: 'lumOff', value: 0.6 }] } stays linked to the theme. getShapeRunFormat reports them beside the unresolved color; the effective readers resolve them into #RRGGBB instead.

Runs that name no face fall back to the theme's font scheme, which is also where a per-script list (<a:font script="Thai" typeface="Cordia New"/> and 46 siblings) lives. createPresentation's blank deck carries the reference desktop app's own default list in both majorFont and minorFont.

Authored XML text and attribute values must contain only XML 1.0 characters. Illegal C0 controls (except tab, LF, and CR), U+FFFE, U+FFFF, and unpaired UTF-16 surrogates throw an error identifying the code point. Remove these characters before authoring; valid supplementary characters such as emoji are preserved.

Object geometry can be locked like the reference desktop app's Selection Pane:

import { isShapeLocked, setShapeLocked } from '@office-kit/pptx';

setShapeLocked(shape, true);
console.log(isShapeLocked(shape)); // true
setShapeLocked([shape, anotherShape], false);

Locks leave text editable and preserve unrelated drawing constraints. Group children keep independent locks; include descendants when locking every object. These APIs write drawing restrictions for editing applications; programmatic geometry setters remain available.

Preview and text-overflow checks

@office-kit/pptx-preview is a companion package that renders a slide to SVG (browser and Node) or to PNG (Node, via resvg, with no headless desktop app). auditTextLayout reports text that overflows its box or wraps unexpectedly, which is how an automated pipeline catches a broken slide before a person sees it.

import { getSlides } from '@office-kit/pptx';
import { renderSlideToSvg } from '@office-kit/pptx-preview';

const svg = renderSlideToSvg(pres, getSlides(pres)[0]!);

Office Kit

@office-kit/pptx is one of three libraries built on the same rules: the ECMA-376 spec is the source of truth, output has to validate, and one ESM build has to run everywhere.

| Package | Files | | -------------------------------------------------------- | --------------------- | | @office-kit/pptx | Presentations .pptx | | @office-kit/xlsx | Spreadsheets .xlsx | | @office-kit/docx | Documents .docx |

Compatibility

  • Node: >= 22.18 (CI runs 22, 24, and 26).
  • Browsers: current and current-1 of Chrome, Firefox, Safari, Edge.
  • TypeScript: >= 5.4 (for strict satisfies and const type parameters).
  • Output: validated with the Open XML SDK and the ECMA-376 schemas (see How output is checked), and smoke-tested against the reference desktop app (current), Keynote (current), Google Slides, and LibreOffice Impress.

Development

git clone --recurse-submodules [email protected]:office-kit/pptx.git
cd pptx
pnpm install
pnpm test

If you already cloned without submodules:

git submodule update --init --recursive --depth 1

references/ holds reference implementations and spec material we read while building this library. See references/README.md.

Contributing

Before opening an issue or PR, please read CLAUDE.md — it documents the project's design rules, the "one way to do one thing" policy, and what counts as a real bug report vs. a low-effort AI-generated one.

PRs are expected to:

  • Follow the template (.github/pull_request_template.md).
  • Include a failing test in the same PR that the change makes pass.
  • Add a changeset (pnpm changeset) for user-visible changes.
  • Pass pnpm typecheck, pnpm lint, and pnpm test.

License

MIT

Trademarks

Not affiliated with or endorsed by Microsoft. PowerPoint is a trademark of the Microsoft group of companies.

Keynote is a trademark of Apple Inc., and Google Slides is a trademark of Google LLC. Office Kit is an independent open-source project and is not affiliated with, sponsored by, or endorsed by either; these names are used only to describe file-format compatibility.