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 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 devOpen 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.tsxnpm 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 devRun 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.
- 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
OpenXmlValidatorgates 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, orzlibon 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
Emunumbers, so inches and points cannot be mixed up by accident.
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.
// 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.
- Open XML SDK. A CI job generates the sample decks and runs the Open XML
SDK's
OpenXmlValidatorover them (tools/ooxml-validate). Any validation error fails the build. - ECMA-376 XSDs. Emitted XML is validated against the official schemas
with
xmllintin the test suite (these tests skip on a machine withoutxmllint). - Real files and a parity corpus. Fixture tests round-trip decks written
by python-pptx and PptxGenJS, and
test/corpusauthors 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.
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-previewpackage 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.
npm install @office-kit/pptx
# or
pnpm add @office-kit/pptx
# or
yarn add @office-kit/pptx| 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. |
@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);Install the Claude Code plugin once from the shared Office Kit marketplace:
claude plugin marketplace add office-kit/skills
claude plugin install pptx@office-kitIn 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.
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' })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);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);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);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.
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);import { loadPresentationFile, savePresentationToFile } from '@office-kit/pptx/node';
const pres = await loadPresentationFile('./template.pptx');
await savePresentationToFile(pres, './out.pptx');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.
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'.
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.
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
});import { setShapeGradientFill } from '@office-kit/pptx';
setShapeGradientFill(shape, {
stops: [
{ offset: 0, color: '#FF0000' },
{ offset: 1, color: '#0000FF' },
],
angleDeg: 90, // top → bottom
});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.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.
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.
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);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.
@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/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 |
- Node: >= 22.18 (CI runs 22, 24, and 26).
- Browsers: current and current-1 of Chrome, Firefox, Safari, Edge.
- TypeScript: >= 5.4 (for strict
satisfiesandconsttype 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.
git clone --recurse-submodules git@github.com:office-kit/pptx.git
cd pptx
pnpm install
pnpm testIf you already cloned without submodules:
git submodule update --init --recursive --depth 1references/ holds reference implementations and spec material we read
while building this library. See references/README.md.
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, andpnpm test.
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.