@extend-ai/react-pptx
Browser-native PowerPoint viewing for React, with experimental OpenDocument support.
npm install @extend-ai/react-pptx
React and React DOM are peer dependencies already present in a React application.
Main API
ReactPptxViewerrenders URLs,Blob,ArrayBuffer,Uint8Array, parsed presentations, or normalized documents.parsePresentationperforms native parsing and returns the same normalized model the viewer renders.initWasmandsetWasmSourcesupport eager initialization and custom Wasm/CDN hosting.PptxViewerControllerprovides navigation, zoom, fit, text search, highlighting, model access, and detached thumbnail rendering.
Import @extend-ai/react-pptx/styles.css alongside the component; the stylesheet is emitted as
a separate package asset. Parsing and rendering are client-side; importing the JavaScript package
during SSR is safe.
Unknown or degraded content is surfaced through onWarning and showDiagnostics rather
than silently removed.
Responsive zoom
The viewer accepts the same responsive zoom shape as @extend-ai/react-docx. Numeric values are
percentages, while automatic, fit-page, and fit-width remain active as the viewport changes.
For example, the shared API can be expressed as:
<Viewer defaultZoom="fit-width" />
With this package's component and required source, use:
<ReactPptxViewer
source={file}
defaultZoom="fit-width"
onZoomChange={({ level, resolvedZoom }) => {
console.log(level, resolvedZoom);
}}
/>
automatic shrinks slides that are wider than the viewport without enlarging them. fit-width
may enlarge or shrink to fill the available width, and fit-page uses both viewport dimensions.
The controller exposes setZoom(level), getZoom(), and getResolvedZoom(), so a custom toolbar
can retain the semantic mode while displaying its current percentage. Zooming in or out from a
responsive mode should pass getResolvedZoom() + delta to setZoom(), which switches to a numeric
level. The older fitMode prop remains supported for compatibility; contain maps to automatic
when the new zoom props are absent.
Supported presentation formats
.pptx and legacy PowerPoint 97–2003 .ppt files are both accepted by the same source
prop and parsePresentation() API. Legacy files are opened natively from OLE compound
storage by the Rust/Wasm parser; the browser does not need LibreOffice, a server-side
conversion endpoint, or a temporary .pptx copy.
For .ppt, the parser follows the Current User/UserEditAtom edit chain and live
persist-object directory to recover the actual slide list. Supported OfficeArt group and
shape anchors, text, pictures, and referenced master drawing content are normalized into
the same PresentationDocument consumed by the React viewer. PNG/JPEG pictures render
directly, while decoded EMF/WMF records use a bounded browser-side image conversion for
static previews.
Legacy rendering is static, not full PowerPoint emulation. Advanced effects, editable
binary charts and tables, animations, transitions, active OLE objects, and timed media may
degrade to a static picture or remain unsupported. Inspect onWarning or enable
showDiagnostics to receive explicit degraded-rendering warnings:
<ReactPptxViewer
source={file}
showDiagnostics
onWarning={(warning) => {
if (warning.code === 'degraded-rendering') {
console.warn(warning.message, warning.slideIndex);
}
}}
/>
The repository regression suite opens a real three-slide PowerPoint 97–2003 file at
tests/fixtures/legacy/file-example-250kb.ppt directly in Rust and the playground. It
checks slide order, text, normalized nodes, thumbnail navigation, and diagnostics without
a conversion step.
Experimental OpenDocument support
OpenDocument presentations (.odp, .otp, .fodp) and drawings (.odg, .otg, .fodg)
are accepted by the same source prop and parsePresentation() API. The Rust/Wasm parser
detects the format from the bytes and returns document.format as odp or odg. Drawing
pages are displayed as slides. These formats are view only.
Supported charts render from their data, and custom drawing shapes use enhanced geometry where supported. Support is experimental: rendering fidelity varies, especially for chart layouts and custom shapes, and some content remains unsupported. See the OpenDocument support guide for details.
WebAssembly asset
Native PowerPoint parsing runs through a WebAssembly module that loads lazily. Most apps can use the default loader. If your bundler or deployment needs to host the binary explicitly, configure it before the first presentation is parsed:
import { setWasmSource } from '@extend-ai/react-pptx';
setWasmSource('https://cdn.example.com/pptx_wasm_bg.wasm');
// A URL, Request, Response, ArrayBuffer/TypedArray, or WebAssembly.Module also works.
The binary is also available as a package subpath for bundlers that support asset URL imports:
import wasmUrl from '@extend-ai/react-pptx/pptx_wasm_bg.wasm?url';
import { setWasmSource } from '@extend-ai/react-pptx';
setWasmSource(wasmUrl);
Configured strings, URLs, request URLs, bytes, and WebAssembly.Module values are forwarded to
the parser worker. A Response is supported on the main thread because it cannot be forwarded
reliably to a worker. initWasm() optionally accepts the same source when eager initialization
is useful.
Examples
See @extend-ai/react-pptx in action in these live Extend UI examples. Click a screenshot to open the demo and integration docs.
PowerPoint Viewer
Explore slide navigation, zoom, and presentation previews.
Fonts
Fonts embedded in a .pptx are decoded by the Rust parser and registered before the first
slide is rendered. Applications can also provide licensed/self-hosted fonts. The viewer
preserves the requested PowerPoint family, then appends Office-compatible and script-complete
fallbacks for Latin, CJK, Arabic, Hebrew, emoji, and math glyphs.
<ReactPptxViewer
source={file}
fonts={{
sources: [
{ family: 'Brand Sans', source: '/fonts/brand-sans.woff2' },
{
family: 'Brand Sans',
source: '/fonts/brand-sans-bold.woff2',
descriptors: { weight: '700' },
},
],
fallbacks: { 'Brand Sans': ['Inter', 'Arial'] },
fallbackFamilies: ['Noto Sans', 'Noto Sans CJK SC', 'Noto Sans Arabic'],
}}
onWarning={(warning) => {
if (warning.code === 'missing-font') console.warn(warning.message);
}}
/>
The viewer bundles shared fallbacks for Calibri, Arial, Times New Roman, Courier New, Aptos and Aptos Narrow. It loads requested families before layout and prefers embedded, caller-provided and installed originals. Each family is a lazy chunk; deploy all generated chunks alongside the viewer. Set fonts.useOfficeFallbacks to false to disable automatic fallback loading. Redistribution notices are in dist/Office-Font-Licenses.txt.
The package does not redistribute proprietary Microsoft fonts. Other families depend on embedded fonts or fonts supplied by the host. resolvePptxFontFamily() and OFFICE_FONT_FALLBACKS are exported for applications that render presentation text outside the viewer.
Custom scroll areas and hooks
Pass the actual scrolling viewport through virtualization.scrollElement. This works with
Radix, Base UI, coss, or any host-owned scroll area and makes that element the observer root.
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null);
const viewer = usePptxViewer();
const parsed = usePptxPresentation(file);
return (
<ScrollArea.Root>
<ScrollArea.Viewport ref={setScrollElement}>
{parsed.presentation ? (
<ReactPptxViewer
ref={viewer.ref}
source={parsed.presentation}
virtualization={{ enabled: true, overscanViewport: 2, scrollElement }}
/>
) : null}
</ScrollArea.Viewport>
</ScrollArea.Root>
);
usePptxPresentation (usePptxModel alias) separates parsing from rendering, while
usePptxViewer exposes the reactive navigation/search/zoom controller. onViewportReady
provides the internal slide surface for advanced integrations and test harnesses.
OfficeArt visuals
Pictures, supported shapes, connectors, and saved SmartArt drawings use the shared Rust OfficeArt renderer. SmartArt text retains its saved text frame separately from the shape frame, including independent offsets, dimensions, rotation, and flips. Semantic data without a supported saved drawing remains an explicit layout fallback.
Presentation metadata
The model carries PowerPoint data beyond the drawn slide so hosts can build their own
behaviour on it. Slides, layouts and masters expose transition and timing (the parsed
p:transition and p:timing parts, including animation nodes, targets and build lists),
text runs expose math (Office Math with a MathML rendering) and field (slide numbers,
dates and other fields with their cached text), table cells expose merge, and every node
may carry clickAction, hoverAction, editing (locks and connector endpoints),
accessibilityTitle, description and decorative. Media nodes expose playback trims
and bookmarks, a poster frame, and embeddedObject details for OLE objects and
controls. The viewer renders math as MathML, skips merged cells, navigates presentation
jump and slide-link actions in place, and shows poster frames with a play control; it
does not play transitions or animation timelines.
parsePresentation(source, { include3dModels: true }) includes original embedded 3D model bytes and content type, model and preview relationships, and scene XML in ImageNode.model3d. The option also works through ReactPptxViewer's parseOptions and the parsing hook. It defaults to false; cached Office raster previews remain the rendered appearance. This metadata does not add an interactive 3D renderer.
Cold-start thumbnails
createPptxThumbnailRenderer renders directly from a ParsedPresentation; it does not mount or
initialize ReactPptxViewer. One renderer reuses its font manager, DOM/SVG rendering context, and
presentation asset cache across slide requests. The parsed presentation can later be passed to
ReactPptxViewer without fetching or parsing it again.
import { createPptxThumbnailRenderer, parsePresentation } from '@extend-ai/react-pptx';
const presentation = await parsePresentation(source, {
onPerformanceMeasurement: recordMeasurement,
});
const renderer = createPptxThumbnailRenderer(presentation, {
fonts: {
loadEmbeddedFonts: false,
waitForFonts: false,
},
concurrency: 2,
onPerformanceMeasurement: recordMeasurement,
});
const thumbnail = await renderer.renderSlide(0, {
maxWidth: 240,
maxHeight: 135,
output: 'blob',
signal: abortController.signal,
});
image.src = URL.createObjectURL(thumbnail.data);
renderer.destroy();
The output can be blob (PNG, the default), canvas, imageBitmap, or svg. renderSlides()
accepts an explicit index list, preserves its order, and never initializes other slides. Both
single and batch requests use the renderer's bounded queue. By default, the thumbnail-specific
font policy skips embedded fonts, global font readiness, and missing-font probing; callers can opt
back into any of those behaviors through fonts.
Performance observers receive download, wasm-parsing, embedded-font-preparation,
slide-dom-svg-rendering, image-decoding, and raster-encoding measurements as applicable. Each
render result also contains its slide-specific measurements.
Thumbnail hook
usePptxViewerThumbnails exposes detached slide previews for a consumer-owned filmstrip. PPTX
thumbnails use the same live DOM/SVG renderer and loaded fonts as the main viewer, so attach each
stable containerRef to a regular element rather than a canvas.
import { ReactPptxViewer, usePptxViewer, usePptxViewerThumbnails } from '@extend-ai/react-pptx';
export function PresentationWithThumbnails({ file }: { file: ArrayBuffer }) {
const viewer = usePptxViewer();
const { thumbnails } = usePptxViewerThumbnails(viewer.controller, {
resolution: { maxWidth: 180, maxHeight: 120 },
renderWindow: {
visibleSlideIndexes: [0, 1, 2],
prefetchSlideIndexes: [3, 4, 5],
},
});
return (
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: 16 }}>
<aside style={{ display: 'grid', gap: 12, alignContent: 'start' }}>
{thumbnails.map((thumbnail) => (
<button
key={thumbnail.slideIndex}
type="button"
onClick={() => void viewer.controller?.goToSlide(thumbnail.slideIndex)}
>
<div
ref={thumbnail.containerRef}
style={{
width: thumbnail.width,
height: thumbnail.height,
overflow: 'hidden',
}}
/>
</button>
))}
</aside>
<ReactPptxViewer ref={viewer.ref} source={file} mode="slide" />
</div>
);
}
The result also provides renderThumbnail(slideIndex, element) for imperative integrations and
rerenderAttachedThumbnails() for refreshing every currently attached preview. disabled keeps
the metadata stable while pausing and disposing thumbnail rendering. For large decks, virtualize
the rail against its actual scroll element: mount containerRef only for virtual rows, pass those
rows through renderWindow.visibleSlideIndexes, and use prefetchSlideIndexes to warm nearby
slides. The built-in showThumbnails filmstrip follows the same windowed behavior.
Block and word citations
The bundled runtime uses the Rust 0.4.1 parser and block extractor, with rust-odf 0.2.0, rust-office-art 0.1.9 and rust-office-charts 0.2.0. Call extractPresentationBlocks(parsed.document, { includeWords: true }), then pass its blocks or words to PptxBlockHighlights or PptxWordHighlights. The components accept a viewer rootRef, selected target, optional multiple targets, color and an onGeometry callback for the selected target, following the React DOCX API.
resolvePptxSourceGeometry, resolvePptxBlockGeometries and resolvePptxWordGeometries return axis-aligned slide-relative fragments with explicit rendered, unmounted, invisible or unresolved states. revealPptxSource(controller, root, ref) mounts and navigates to the referenced slide. normalizedPptxBoundingBox converts a fragment to fractional slide coordinates.
References carry document identity, slide and node paths, optional cell/paragraph indices and UTF-16 text offsets. Preserve them with extracted content; matching visible text alone cannot distinguish repeated occurrences. Geometry is limited to mounted slide content. Notes, chart-label subranges and mathematical subranges do not receive invented rectangles.
See the citation guide for usage and the playground Blocks view.
