GitHub

PPTX

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 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.

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 pptxviewjs presentation, 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.

PropTypeDescription
sourceUrlViewerSource | BlobViewerSourceCanonical presentation source.
scalenumberControlled scale; omit to let the viewer own fit-width and zoom state.
defaultScalenumberInitial uncontrolled scale; omit to start in fit-width mode.
onScaleChange(scale: number | null) => voidCalled by zoom controls in controlled mode. null means fit width.
controlsbooleanShow the zoom/rotate/download controls. Defaults to true.
renderSlideOverlay(props) => ReactNodePer-slide overlay; receives { slideNumber, width, height, scale, rotation }.
onVisibleSlideChange(slide: 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 controls (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. Pass false to defer uncached renders until scrolling settles. Defaults to true.
barebooleanDrop the outer border/background so the viewer fills its container.
classNamestringOptional class on the viewer root element.

Source

pptx-viewer.tsx