Render PDF · Word · Excel · PowerPoint · image · video · audio — entirely in the browser. No Microsoft Online Viewer iframe. No Google Docs preview. No "we upload your file to our server first." The bytes never leave the tab.
'use client', drop in <Viewer />, done. The hook works with Suspense and the App Router.eval, no dangerouslySetInnerHTML of user content.Drop any of these onto the page — it renders locally:
*.pdf*.docx *.doc*.xlsx *.xls *.csv *.tsv *.ods*.pptx*.png *.jpg *.gif *.webp *.svg *.avif *.bmp*.mp4 *.webm *.ogg *.mov *.mkv*.mp3 *.wav *.flac *.aac *.m4a *.opus*.txt *.md *.json *.yaml and every common source-code extension| Package | Version | Min+gzip | Description |
|---|---|---|---|
@microscope-js/core |
Framework-agnostic registry + mount() |
||
@microscope-js/utils |
Source / MIME / sanitize helpers | ||
@microscope-js/react |
React / Next.js adapter, <Viewer /> |
||
@microscope-js/renderer-pdf |
PDF via pdfjs-dist |
||
@microscope-js/renderer-docx |
Word .docx via mammoth + DOMPurify |
||
@microscope-js/renderer-xlsx |
Excel .xlsx/.xls/.csv via SheetJS |
||
@microscope-js/renderer-pptx |
PowerPoint .pptx via JSZip |
||
@microscope-js/renderer-image |
png / jpg / gif / webp / svg / avif / bmp | ||
@microscope-js/renderer-video |
mp4 / webm / ogg / mov | ||
@microscope-js/renderer-audio |
mp3 / wav / flac / ogg / aac | ||
@microscope-js/renderer-text |
Plain text / source code |
pnpm add @microscope-js/react @microscope-js/renderer-pdf @microscope-js/renderer-image
'use client';
import { Viewer, useRegistry } from '@microscope-js/react';
import { pdfRenderer } from '@microscope-js/renderer-pdf';
import { imageRenderer } from '@microscope-js/renderer-image';
export default function FilePreview({ file }: { file: File }) {
const registry = useRegistry([pdfRenderer, imageRenderer]);
return <Viewer source={file} registry={registry} style={{ height: 600 }} />;
}
<Viewer /> accepts a File, Blob, ArrayBuffer, Uint8Array, URL, or a string URL — anything you have on hand.
import { createRegistry, mount } from '@microscope-js/core';
import { pdfRenderer } from '@microscope-js/renderer-pdf';
const registry = createRegistry([pdfRenderer]);
const handle = await mount({
source: fileInput.files[0],
container: document.getElementById('viewer')!,
registry,
});
// later...
handle.destroy();
const { containerRef, loading, error, handle } = useViewer({ source, registry });
return (
<div ref={containerRef} className="my-toolbar-frame">
{handle?.capabilities?.pageCount /* renderer-specific extras */}
</div>
);
| Concern | MS Office iframe | Google Docs viewer | docx-preview / etc. | microscope-js |
|---|---|---|---|---|
| File leaves the device | ✅ uploads to Microsoft | ✅ uploads to Google | ❌ | ❌ |
| Works offline | ❌ | ❌ | partial | ✅ |
| Formats covered | Office only | Office + PDF | one per library | PDF + Office + media + image + code, one API |
| Bundle cost | iframe (heavy) | iframe (heavy) | one library, monolithic | ~1–5 KB core + per-format packages, tree-shakable |
| React + Next.js / SSR | ⚠️ iframe | ⚠️ iframe | varies | ✅ first-class, SSR-safe |
| Provenance / signing | n/a | n/a | usually unsigned | ✅ SLSA + npm trusted publisher |
| License | proprietary | proprietary | mixed | MIT |
┌─────────────────────────────────┐
│ @microscope-js/core │
│ Renderer interface + registry │
└──────────────┬──────────────────┘
│
┌───────────────────────┼──────────────────────────┐
│ │ │
┌──────▼──────┐ ┌───────▼────────┐ ┌───────▼─────────┐
│ renderer-pdf│ ... │ renderer-image │ ... │ renderer-docx │
└─────────────┘ └────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼──────────────────────────┘
│
┌──────────────▼──────────────────┐
│ @microscope-js/react │
│ <Viewer />, useViewer() │
└─────────────────────────────────┘
The core package defines exactly one interface (Renderer) and one registry. Every format implementation lives in its own npm package and is loaded via dynamic import() so first-paint pays only for core + react.
| Threat | Mitigation |
|---|---|
| HTML in Office documents | All renderer output passes through DOMPurify with a hardened allowlist |
Zip-slip (.. in DOCX/XLSX/PPTX) |
Archive entries with parent-dir or absolute paths are rejected before extraction |
| Zip bombs | ByteBudget caps total uncompressed bytes (default 512 MB) |
| Oversized PDFs | Configurable maxBytes / maxPages per renderer |
| SVG / XML XSS | SVGs are mounted via <img> (no script execution); inline SVG is sanitized |
| Dynamic JS | No eval, no new Function, no dangerouslySetInnerHTML of user content — CI greps for these |
| Supply-chain | Every published tarball is signed with SLSA provenance via npm OIDC trusted publishing |
See SECURITY.md to report a vulnerability privately.
pnpm install
pnpm build # build all packages
pnpm test # vitest across the monorepo
pnpm typecheck # tsc --noEmit, per package
pnpm lint # biome check
pnpm demo # localhost:3000
pnpm docs # TypeDoc -> ./docs-site
microscope-js/
├── packages/
│ ├── core/ # Renderer interface + registry
│ ├── utils/ # source / sniff / sanitize / zip-safety helpers
│ ├── react/ # <Viewer /> + useViewer + useRegistry
│ ├── renderer-pdf/
│ ├── renderer-docx/
│ ├── renderer-xlsx/
│ ├── renderer-pptx/
│ ├── renderer-image/
│ ├── renderer-video/
│ ├── renderer-audio/
│ └── renderer-text/
├── apps/
│ └── demo/ # Next.js → GitHub Pages
└── .github/workflows/ # CI · release · pages · security · dependabot
Got an idea? Open a discussion.
PRs are welcome — see CONTRIBUTING.md. Adding a new format is a copy of packages/renderer-image with a new Renderer object. The registry picks it up automatically.
If this saved you a backend, a star on GitHub goes a long way. ⭐
MIT © microscope-js contributors
Built with pnpm · tsup · Changesets · Biome · Vitest · TypeDoc. Published with npm Trusted Publishers + SLSA provenance.