Skip to content

Repository files navigation

@office-kit/pptx

Generate and edit .pptx (PowerPoint / Office Open XML Presentation) files from TypeScript — in Node.js or the browser, from a single ESM bundle.

Status: 0.x — pre-1.0, public API still evolving. The capabilities in the table below are exercised against real PPTX fixtures, and emitted XML is checked against the ECMA-376 schemas with xmllint where it is available on the machine running the tests. Until the 1.0 release the public API is not frozen — breaking changes can land in a minor (0.x) release, so pin a version or an exact range.

Why

The JavaScript ecosystem has several PPTX libraries, but they typically pick one trade-off:

  • Node-only with a Buffer-shaped API → does not work in the browser.
  • Browser-only wrapping a fixed template → cannot author from scratch.
  • Loose XML strings that "usually open" → break in Keynote / Google Slides / the Open XML SDK validator.

@office-kit/pptx is built around a different stance:

  • One ESM bundle that runs in Node and the browser.
  • A typed object model that mirrors the OOXML PresentationML spec (ECMA-376 Part 1, §19). When the spec says something is a choice, our types say it is a discriminated union.
  • Output that passes Microsoft's Open XML SDK Productivity Tool validator, not just PowerPoint's "open and pray."
  • Two complementary paths: author from scratchoredit a template.

Scope

The work is split into four levels of completeness. The current 0.x line covers levels 1-3 in full and level 4 in part; the table tracks where each capability stands today. Items marked "post-1.0" are not implemented yet:

LevelCapability0.x
L1Read an existing PPTX, save it back without corruption
L2Template edit — text replacement, image swap, add slide from layout
L3Authoring — shapes, text, tables, fills, effects, transforms
L3Authoring on top of existing themes / masters / layouts
L3Constructing new themes / masters / layouts from scratch❌ post-1.0
L3Charts (all common types) with embedded data
L4Notes, comments, transitions
L4Simple animations (entrance / exit / emphasis presets)
L4SmartArt authoring❌ post-1.0 (read pass-through)
L4Complex animation timing trees❌ post-1.0
L4OLE / ActiveX authoring❌ post-1.0 (read pass-through)
L4Document encryption (read + write)❌ post-1.0

Out-of-scope content is still preserved on round-trip@office-kit/pptx will never silently strip parts it doesn't model. That's 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 — its closeness to LibreOffice is measured per slide and gated in CI (site/fidelity) — but it is a high-fidelity preview, not a spec-complete paint engine. For pixel-authoritative output, use PowerPoint itself or LibreOffice headless.
  • You need a thin DSL for one-off "report" slides and do not care about schema validity. A simpler library will be lighter.
  • You want to convert PPTX to another format (Keynote, ODP). Out of scope forever — that's a renderer's job.

Install

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

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 ~60 KB.

import{findSlidePlaceholder,getSlides,loadPresentation,savePresentation,setShapeText,}from'@office-kit/pptx';constpres=awaitloadPresentation(bytes);consttitle=findSlidePlaceholder(getSlides(pres)[0]!,'title');if(title)setShapeText(title,'Hello');constout=awaitsavePresentation(pres);

Driving @office-kit/pptx from an AI agent

skill/SKILL.md is a self-contained guide for an LLM agent authoring presentations with this library: the canonical call for each capability, the design rules that keep output from looking template-generated, the handful of API footguns worth memorizing, and a QA loop to run before declaring a deck done. Its worked example is exercised by the test suite, so the code there is known to produce a schema-valid deck.

CI enforces the tree-shake bound in test/tree-shake.test.ts.

Usage

Edit a template

import{findSlidePlaceholder,getSlides,loadPresentation,savePresentation,setShapeText,}from'@office-kit/pptx';constpres=awaitloadPresentation(existingPptxBytes);constcover=getSlides(pres)[0]!;consttitle=findSlidePlaceholder(cover,'title');if(title)setShapeText(title,'Q3 Review');constbody=findSlidePlaceholder(cover,'body');if(body)setShapeText(body,'Numbers up and to the right.');constout: Uint8Array=awaitsavePresentation(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';constpres=awaitloadPresentation(templateBytes);// Replaces `{{name}}`, `{{event}}`, `{{date}}` across every slide.replaceTokensInPresentation(pres,{name: 'Alice',event: 'Re:Invent',date: '2026-12-01'});constout=awaitsavePresentation(pres);

Build a deck from scratch (no template file)

createPresentation() returns an immediately-authorable deck — a slide master, the Office 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.constpres=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 PowerPoint UI languages. findSlideLayout(pres, 'Blank') matches// the user-visible name, which is case-sensitive and localized.consttitleLayout=findSlideLayoutByType(pres,'title')!;constslide=addSlide(pres,{layout: titleLayout});setShapeText(findSlidePlaceholder(slide,'ctrTitle')!,'Authored with @office-kit/pptx');constout: Uint8Array=awaitsavePresentation(pres);

Build a deck from a blank template

import{addSlide,addSlideImage,addSlideTextBox,duplicateSlide,findSlideLayout,findSlidePlaceholder,inches,loadPresentation,moveSlide,savePresentation,setShapeText,}from'@office-kit/pptx';constpres=awaitloadPresentation(awaitfetch('/blank.pptx').then((r)=>r.arrayBuffer()));consttitleLayout=findSlideLayout(pres,'Title Slide')!;constslide1=addSlide(pres,{layout: titleLayout});setShapeText(findSlidePlaceholder(slide1,'ctrTitle')!,'@office-kit/pptx demo');setShapeText(findSlidePlaceholder(slide1,'subTitle')!,'an OOXML library for TypeScript');constblank=findSlideLayout(pres,'Blank')!;constslide2=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)});constdup=duplicateSlide(pres,slide2);moveSlide(pres,dup,0);constout: Uint8Array=awaitsavePresentation(pres);

Replace an image in place

import{getShapeKind,getShapeName,getSlideShapes,getSlides,loadPresentation,savePresentation,setShapeImage,}from'@office-kit/pptx';constpres=awaitloadPresentation(templateBytes);for(constslideofgetSlides(pres)){for(constshapeofgetSlideShapes(slide)){if(getShapeKind(shape)==='picture'&&getShapeName(shape)==='Logo'){setShapeImage(shape,newLogoBytes);// format auto-detected; geometry preserved}}}constout=awaitsavePresentation(pres);

Node convenience entry

import{loadPresentationFile,savePresentationToFile}from'@office-kit/pptx/node';constpres=awaitloadPresentationFile('./template.pptx');awaitsavePresentationToFile(pres,'./out.pptx');

Charts

import{addSlideChart,getSlides,loadPresentation,savePresentation,inches,}from'@office-kit/pptx';constpres=awaitloadPresentation(templateBytes);constslide=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 | areacategories: ['Q1','Q2','Q3','Q4'],series: [{name: 'Revenue',values: [120,180,240,300]},{name: 'Cost',values: [80,90,130,160]},],title: 'FY26 plan',},});awaitsavePresentation(pres);

The embedded xlsx that PowerPoint 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';constslide=getSlides(pres)[0]!;constshape=getSlideShapes(slide)[0]!;setShapeAnimation(shape,{effect: 'fadeIn',durationMs: 800});// effects: 'fadeIn' | 'fadeOut' | 'appear' | 'disappear'

Comments

import{addSlideComment,getSlides}from'@office-kit/pptx';constslide=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';constissues=validatePresentation(pres);for(constiofissues)console.error(i.severity,i.message);// Catches missing rels, dangling slide ids, layouts without masters, etc.

API surface (current state)

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

CapabilityAPI
Load / saveloadPresentation(input), savePresentation(pres), loadPresentationFile(path) (node), savePresentationToFile(pres, path) (node)
CreatecreatePresentation({ size?: '16:9' | '4:3' }) — blank deck with master + theme + Blank / Title Slide / Title and Content layouts
Slide CRUDgetSlides, getSlideAt, getSlideIndex, addSlide, removeSlide, moveSlide, duplicateSlide, clearSlideShapes
Slide layoutgetSlideLayouts, 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 metadatagetSlideTitle / setSlideTitle, getSlideSize / setSlideSize, isSlideHidden / setSlideHidden, getSlideText
Slide sectionsgetSlideSections, setSlideSections (p14 sectionLst)
PlaceholdersfindSlidePlaceholder(slide, 'title' | 'body' | ...)
Token / text replacereplaceTokensInPresentation, replaceTokensInSlide, replaceTextInPresentation, replaceTextInSlide
BackgroundgetSlideBackground / setSlideBackground / clearSlideBackground
NotesgetSlideNotes / setSlideNotes
TransitionsgetSlideTransition / setSlideTransition / clearSlideTransition
AnimationsgetShapeAnimation / setShapeAnimation (fadeIn / fadeOut / appear / disappear), clearSlideAnimations
CommentsaddSlideComment, getSlideComments, removeSlideComment, getCommentAuthors, getCommentText / getCommentAuthor / getCommentPosition
Shape authoringaddSlideTextBox, addSlideShape, addSlideLine, addSlideTable, addSlideImage, addSlideChart
Shape lookupfindShapeByName, findShapesByName, findShapesByKind, findShapeInPresentation, getAllShapes, getSlideShapes
Shape textsetShapeText, setShapeBullets, setShapeAlignment, setShapeTextFormat, setShapeHyperlink / getShapeHyperlink
Per-paragraphsetParagraphAlignment / getParagraphAlignment, setParagraphLevel / getParagraphLevel, setParagraphBullet / getParagraphBullet, setParagraphSpacing / getParagraphSpacing, setParagraphLineSpacing / getParagraphLineSpacing
Per-run textsetShapeRunText / getShapeRunText, setShapeRunFormat / getShapeRunFormat, getShapeParagraphCount, getShapeRunCount
Text framesetShapeTextAnchor / getShapeTextAnchor, setShapeTextMargins / getShapeTextMargins
FillsetShapeFill / getShapeFill, setShapeGradientFill, setShapePatternFill, setShapeImageFill, setShapeNoFill, clearShapeFill
StrokesetShapeStroke / getShapeStroke, setShapeStrokeDash / getShapeStrokeDash, setShapeStrokeArrow / getShapeStrokeArrow, …NoStroke
EffectssetShapeShadow / setShapeGlow / getShapeEffect, clearShapeEffects
GeometrysetShapePosition, setShapeSize, setShapeRotation, setShapeFlip, setShapeBounds / getShapeBounds
PicturessetShapeImage, setShapeImageCrop / getShapeImageCrop, setShapeImageOpacity / getShapeImageOpacity, setShapeImageBrightness, …Contrast
Z-orderbringShapeToFront, sendShapeToBack, bringShapeForward, sendShapeBackward
Click actionssetShapeClickAction / getShapeClickAction (url / slide / nextSlide / prevSlide / firstSlide / lastSlide)
Shape removalremoveShape
TablesgetTableCell / getTableCells, setTableCellText / getTableCellText, setTableCellFill / clearTableCellFill, setTableCellAlignment, setTableCellTextFormat, insertTableRow / removeTableRow, insertTableColumn / removeTableColumn
ChartsaddSlideChart, getSlideCharts, setChartSpec — kinds: bar, column, line, pie, doughnut, area
ThemegetPresentationTheme — color scheme (accent1..accent6, dark1, light1, hyperlink, ...)
ValidationvalidatePresentation(pres) — invariant checks, returns ValidationIssue[]
Unitsinches(n), cm(n), mm(n), pt(n), emu(n) — return branded Emu numbers

Compatibility

  • Node: >= 24.16.
  • Browsers: current and current-1 of Chrome, Firefox, Safari, Edge.
  • TypeScript: >= 5.4 (for strict satisfies and const type parameters).
  • Output: PPTX files checked against the ECMA-376 schemas with xmllint where it is available, and smoke-tested against PowerPoint (current), Keynote (current), Google Slides, and LibreOffice Impress.

Development

git clone --recurse-submodules git@github.com:office-kit/pptx.git
cd @office-kit/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

About

A JavaScript library for PowerPoint (PPTX) files

Resources

Security policy

Stars

10 stars

Watchers

2 watching

Forks

Releases

Packages

Contributors

Languages