GitHub

PPTX Viewer

A canvas-backed PowerPoint viewer with zoom, rotate, fit-to-width, download, and a per-slide overlay slot — rendered entirely client-side, no server conversion.

The PPTX Viewer renders a PowerPoint deck the same way the PDF Viewer renders pages: 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.

It is built without useEffect: the deck loads with React's use() and Suspense, slides render lazily via an IntersectionObserver (only slides near the viewport hit the canvas), and renders are serialized so they never race.

Installation

pnpm dlx shadcn@latest add @retab/pptx-viewer

This pulls in pptxviewjs and its jszip peer (used both to unzip the deck and to read the slide size). 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 src="/deck.pptx" className="h-[600px]" />
}

Bounding-box overlays

renderPageOverlay runs per slide and receives the rendered slide size, so normalized boxes map with simple percentages:

<PptxViewer
  src={url}
  renderPageOverlay={({ pageNumber }) =>
    citationsOnSlide(pageNumber).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 straight from presentation.xml, so every slide reserves its box before anything is drawn.
  • 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.
  • 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 deferral. While the user is actively scrolling, a slide's (expensive) first render is held until scrolling settles, so a fast fling stays at frame rate instead of competing with on-the-fly renders. The initial paint, a settled scroll, and already-cached slides all render immediately. Pass eager to render slides the moment they near the viewport instead.

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.

Props

The API mirrors the PDF ViewerpageNumber is a 1-based slide index.

PropTypeDescription
srcstringURL of the .pptx (same-origin or CORS-enabled).
scalenumberOptional fixed scale; omit to fit slide width to the container.
toolbarbooleanShow the zoom/rotate/download toolbar. Defaults to true.
downloadFileNamestringFilename for the download button.
renderPageOverlay(props) => ReactNodePer-slide overlay; receives { pageNumber, width, height, scale, rotation }.
onVisiblePageChange(page: number) => voidFires with the slide nearest the top of the viewport on scroll.
onScrollProgressChange(progress: number) => voidFires with scroll progress in [0, 1].
headerReactNodeFull-width strip below the toolbar (e.g. a legend).
asideReactNodeLeft rail alongside the scrolling slides (e.g. a thumbnail rail).
eagerbooleanRender slides the moment they near the viewport, even mid-scroll, instead of deferring until scrolling settles. Defaults to false.
barebooleanDrop the outer border/background so the viewer fills its container.
classNamestringOptional class on the viewer shell.