- Overview
- Anatomy
- Header
- Navigation
- Renderers
The PPTX Viewer renders a PowerPoint deck as continuous slides: each slide is
drawn to its own <canvas>. It parses the .pptx
(an Office Open XML zip) entirely in the browser with
pptxviewjs — a dependency-light,
canvas-rendering library — so there is no server, no upload, and no LibreOffice
conversion step.
It does continuous-scroll rendering, zoom, rotate, fit-to-width, and download, and exposes a per-slide overlay slot so you can draw bounding-box citations on top of the rendered slide.
The deck loads with React's use() and Suspense, a math-based virtual slide
window keeps the DOM bounded, and renders are serialized so they never race.
"use client";
import * as React from "react";
import { PptxViewer } from "@/components/ui/pptx-viewer";
// A normalized bounding box (0..1) to demonstrate the per-slide overlay slot —
// the same shape Retab's edit fields and extraction sources use. Anchored to the
// title bar on the first slide of the sample deck.
const sampleBox = { slide: 1, left: 0.0, top: 0.0, width: 1.0, height: 0.187 };Installation
pnpm dlx shadcn@latest add @retab/pptx-viewer
This pulls in pptxviewjs. chart.js is also installed because the library
imports it statically; it is only exercised when a deck actually contains charts.
Usage
import { PptxViewer } from "@/components/ui/pptx-viewer";
export function Example() {
return (
<PptxViewer
source={{ kind: "url", url: "/deck.pptx", fileName: "deck.pptx" }}
className="h-[600px]"
/>
);
}Bounding-box overlays
renderSlideOverlay runs per slide and receives the rendered slide size, so
normalized boxes map with simple percentages:
<PptxViewer
source={{ kind: "url", url, fileName: "deck.pptx" }}
renderSlideOverlay={({ slideNumber }) =>
citationsOnSlide(slideNumber).map((c) => (
<div
key={c.key}
className="absolute outline outline-2 outline-indigo-500"
style={{
left: `${c.bbox.left * 100}%`,
top: `${c.bbox.top * 100}%`,
width: `${c.bbox.width * 100}%`,
height: `${c.bbox.height * 100}%`,
}}
/>
))
}
/>How it performs
- Canvas rendering, client-side. Slides parse and rasterize in the browser —
no conversion service. The slide size is read from the loaded
pptxviewjspresentation, so every slide reserves its box before anything is drawn without reopening the archive. - Lazy slides. Only slides near the viewport render; off-screen canvases are dropped, keeping a long deck bounded.
- Bitmap cache. Each rendered slide is cached as an
ImageBitmap(a capped LRU keyed by slide + zoom), so scroll-back and revisited zoom levels redraw instantly instead of re-running the renderer. - Adaptive render scale. The viewer caps high-DPI raster work while preserving zoom, avoiding runaway canvas costs on dense displays.
- Serialized, cancellable renders. The underlying viewer holds one drawing context, so heavy renders are queued — never overlapping, never racing — and a queued render is skipped if its slide has already scrolled away, so fast-scrolling never wastes the queue on stale slides.
- Scroll-aware rendering. Slides render as soon as they near the viewport, so
visible content appears immediately. Pass
eager={false}for very large decks when uncached slide renders should wait until scrolling settles. - Shared thumbnail source. File thumbnails reuse the same parsed deck source and bitmap cache as the full viewer instead of parsing the presentation a second time.
- Bounded source ownership. Parsed deck sources and rendered bitmaps are kept in a small LRU and disposed when evicted, so browsing many presentations does not leave old decks resident indefinitely.
Fidelity
pptxviewjs covers the common deck — text, shapes, tables, images, and charts —
but it is not a pixel-perfect PowerPoint clone. Exotic SmartArt, transitions, and
embedded media may render approximately or be omitted. For pixel-exact output,
convert to PDF server-side and use the PDF Viewer.
API Reference
The PPTX overlay field is slideNumber, a 1-based slide index.
| Prop | Type | Description |
|---|---|---|
source | UrlViewerSource | BlobViewerSource | Canonical presentation source. |
scale | number | Controlled scale; omit to let the viewer own fit-width and zoom state. |
defaultScale | number | Initial uncontrolled scale; omit to start in fit-width mode. |
onScaleChange | (scale: number | null) => void | Called by zoom controls in controlled mode. null means fit width. |
controls | boolean | Show the zoom/rotate/download controls. Defaults to true. |
renderSlideOverlay | (props) => ReactNode | Per-slide overlay; receives { slideNumber, width, height, scale, rotation }. |
onVisibleSlideChange | (slide: number) => void | Fires with the slide nearest the top of the viewport on scroll. |
onScrollProgressChange | (progress: number) => void | Fires with scroll progress in [0, 1]. |
header | ReactNode | Full-width strip below the controls (e.g. a legend). |
aside | ReactNode | Left rail alongside the scrolling slides (e.g. a thumbnail rail). |
eager | boolean | Render slides the moment they near the viewport, even mid-scroll. Pass false to defer uncached renders until scrolling settles. Defaults to true. |
bare | boolean | Drop the outer border/background so the viewer fills its container. |
className | string | Optional class on the viewer root element. |
Source
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
import { joinEffectKey } from "@/lib/effect-key";
import { useKeyedLayoutEffect } from "@/hooks/use-keyed-layout-effect";
import { useMountEffect } from "@/hooks/use-mount-effect";
import {
createViewerResource,
type ViewerResource,
} from "@/lib/viewer-resource";