npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

omni-doc-viewer

v0.1.3

Published

One <DocViewer> for PDF, Word, Excel & PowerPoint. 100% client-side — no server, no Microsoft iframe, no public-URL requirement, no API keys.

Readme

omni-doc-viewer

npm version unpacked size types included license: MIT

One <DocViewer> for PDF, Word, Excel, PowerPoint, images, Markdown, CSV & text — 100% in the browser. No server. No Microsoft Office iframe. No public-URL requirement. No API keys.

▶ Live demo — drop in a file or try a sample. Works offline.

import { DocViewer } from 'omni-doc-viewer/react'

<DocViewer source={file} pagination />

Table of contents


Why omni-doc-viewer

Most "free" document viewers force a bad trade-off. They either:

  • embed Microsoft / Google Office Online in an iframe — which needs the internet and a publicly reachable file URL, so it's useless for private, local, offline, or auth-gated files; or
  • are paid commercial SDKs; or
  • are a pile of separate single-format libraries you have to find, wire together, and maintain yourself.

omni-doc-viewer wraps proven, permissively-licensed rendering engines behind one API, with automatic format detection and per-format lazy loading. It runs entirely in the browser — works offline, no backend, no keys, no uploads.

 source (URL │ File │ Blob │ ArrayBuffer │ Uint8Array)
        │
        ▼
   normalize ──▶ detect ──▶ lazy-load the one engine ──▶ render ──▶ RenderResult
   (→ bytes)    (type)      (only what you use)          (into DOM)  (+ destroy())

Features

  • 8 formats, one component — PDF, Word (.docx), Excel (.xlsx/.xls), PowerPoint (.pptx), images, Markdown, CSV/TSV, and plain text.
  • 100% client-side — no server, no Office iframe, no public URL, no API keys. Works offline.
  • Automatic format detection — by file extension and by magic bytes, so it works even with no filename.
  • Selectable, searchable PDFs — a real text layer means copy/paste, browser Ctrl+F, and screen readers all work.
  • Virtualized rendering — large PDFs only rasterize the pages near the viewport, so a 500-page file stays fast and memory-light.
  • Opt-in page navigation — a polished, responsive toolbar: prev/next, jump-to-page, zoom, fit-width, download, print, and a paged ⇄ continuous toggle.
  • Real images in PowerPoint — EMF/WMF metafiles are rasterized to PNG, and layout-inherited picture placeholders are positioned correctly (cases most viewers drop).
  • Safe Markdown — rendered HTML is sanitized with DOMPurify (no XSS from untrusted files).
  • Per-format lazy loading — a consumer who only shows PDFs never ships the Excel/PowerPoint/Markdown code.
  • Framework-agnostic core + optional React wrapper — use it anywhere, or drop in <DocViewer />.
  • First-class TypeScript — full types, no @types package needed.
  • SSR-safe — inert on the server, renders on the client.

Supported formats

| Format | Extensions | Engine | Renders to | |---|---|---|---| | PDF | .pdf | pdfjs-dist | <canvas> per page + selectable text layer, virtualized | | Word | .docx | docx-preview | semantic, paginated HTML | | Excel | .xlsx, .xls | SheetJS | HTML table + sheet tabs | | PowerPoint | .pptx | pptx-preview + emf-converter | HTML/CSS slides (EMF/WMF rasterized) | | Image | .png .jpg .jpeg .gif .webp .svg .bmp .avif .ico | native | <img> | | Markdown | .md .markdown .mdown .mkd | marked + DOMPurify | sanitized HTML | | CSV / TSV | .csv .tsv | built-in (RFC 4180) | HTML table with sticky header | | Text | .txt .log, or any UTF-8 text | built-in | <pre> (wrapped) |

Out of scope

  • Legacy binary .doc / .ppt (pre-2007 OLE). These need a server-side converter. Passing one throws an UnsupportedFormatError with a clear message. (.xls does work — SheetJS parses it.)
  • PowerPoint fidelity: slides are a readable preview — animations, transitions, 3D, charts, SmartArt, OLE objects and speaker notes are not reproduced.

Installation

npm install omni-doc-viewer
# or
pnpm add omni-doc-viewer
# or
yarn add omni-doc-viewer
# or
bun add omni-doc-viewer
  • React is optional. It's a peer dependency you only need if you import omni-doc-viewer/react. The core (omni-doc-viewer) has no UI-framework dependency.
  • ESM and CommonJS builds are both shipped, with TypeScript declarations.
  • Node ≥ 18 to build/develop; at runtime it targets evergreen browsers.
// package.json "exports"
"."        // → renderDocument, detect, types, … (framework-agnostic)
"./react"  // → <DocViewer />, DocViewerProps, …

Quick start

React

'use client'
import { useState } from 'react'
import { DocViewer } from 'omni-doc-viewer/react'

export function Example() {
  const [file, setFile] = useState<File | string>('/docs/report.pdf')

  return (
    <>
      <input
        type="file"
        accept=".pdf,.docx,.xlsx,.pptx,.png,.jpg,.svg,.md,.csv,.txt"
        onChange={(e) => e.target.files?.[0] && setFile(e.target.files[0])}
      />

      <DocViewer
        source={file}            // URL | File | Blob | ArrayBuffer | Uint8Array
        pagination               // page-by-page toolbar (optional)
        style={{ height: '80vh' }}
        onLoad={(meta) => console.log('loaded', meta.type, meta.pageCount)}
        onError={(err) => console.error(err)}
      />
    </>
  )
}

Without a framework

The core gives you an element + a source. It works with any framework, or none.

import { renderDocument } from 'omni-doc-viewer'

const container = document.getElementById('viewer')!

const view = await renderDocument({
  container,                 // contents are replaced
  source: fileOrUrlOrBuffer, // anything in DocSource
})

console.log(view.type, view.meta.pageCount)

// later — free engine resources, listeners and DOM:
view.destroy()

Loading a document

source accepts anything in the DocSource union — pick whatever you already have:

type DocSource = string | File | Blob | ArrayBuffer | Uint8Array

| Source | How it's handled | Filename used for detection? | |---|---|---| | string (URL) | fetched (supports http(s): and blob:) | yes — from the URL path | | File | read via arrayBuffer() | yes — file.name | | Blob | read via arrayBuffer() | no — falls back to magic bytes | | ArrayBuffer / Uint8Array | used directly | no — falls back to magic bytes |

// From a URL
renderDocument({ container, source: 'https://example.com/a.pdf' })

// From an <input type="file"> or drag-and-drop
renderDocument({ container, source: event.target.files[0] })

// From bytes you already have (e.g. a decrypted buffer)
renderDocument({ container, source: myUint8Array, type: 'pdf' })

When the source has no filename/extension (a Blob, ArrayBuffer, or an extensionless URL), detection falls back to magic-byte sniffing. If you already know the format, pass type to skip detection entirely.


React component (DocViewer)

import { DocViewer, type DocViewerProps, type ViewMode } from 'omni-doc-viewer/react'

Props

DocViewerProps extends the per-format tuning options (pdf, pptx — see Configuration).

| Prop | Type | Default | Description | |---|---|---|---| | source | DocSource | required | URL string, File, Blob, ArrayBuffer or Uint8Array. | | type | DocType | auto | Force a format and skip auto-detection. | | pagination | boolean | false | Enable the page-navigation toolbar inside a scrollable viewport. | | initialViewMode | 'paged' \| 'continuous' | 'paged' | Layout when pagination is on: one page at a time, or a scrolling stack. | | height | number \| string | '80vh' | Viewport height when pagination is on. Number = px. | | toolbar | boolean | = pagination | Show the toolbar. Set false to keep navigation logic but hide the chrome. | | loading | ReactNode | spinner text | Shown while the engine + document load. | | errorFallback | (error: Error) => ReactNode | message | Render prop for the error state. | | onLoad | (meta: RenderMeta) => void | — | Fired once the document has rendered. | | onError | (error: Error) => void | — | Fired if rendering fails. | | onPageChange | (page: number, total: number) => void | — | Fired when the current page changes (1-based). | | pdf | { scale?, workerSrc?, textLayer? } | — | PDF tuning (details). | | pptx | { width?, height? } | — | PowerPoint slide size. | | className | string | — | Applied to the root element. | | style | CSSProperties | — | Applied to the root element. |

Page navigation (pagination)

Set pagination to turn the plain viewer into a full reading experience. The toolbar gives you:

  • Prev / next and a page number input you can type into (n / total) to jump.
  • Zoom (−, %, +), fit-width, download (original bytes), and print.
  • A paged ⇄ continuous toggle (one page at a time, or a vertical scroll of all pages).
  • Keyboard navigation: /, PageUp/PageDown, Home/End.
  • Vertical and horizontal scrolling (horizontal kicks in when zoomed or for wide slides).
<DocViewer
  source={file}
  pagination
  initialViewMode="paged"     // or "continuous"
  height="80vh"               // or a number of px
  onPageChange={(page, total) => setLabel(`${page} / ${total}`)}
/>

The toolbar is responsive to the viewer's own width (via CSS container queries), so it stays usable in a narrow sidebar or on a phone — not just based on the window size.

Pagination is meaningful for paged formats (PDF, PPTX, DOCX). Spreadsheets navigate by their own sheet tabs; single images/text render as one page.

Custom loading & error UI

<DocViewer
  source={file}
  loading={<MySpinner />}
  errorFallback={(err) => <MyError message={err.message} />}
/>

Lifecycle callbacks

<DocViewer
  source={file}
  onLoad={(meta) => {
    // meta.type:      'pdf' | 'docx' | 'xlsx' | 'pptx' | 'image' | 'markdown' | 'csv' | 'text'
    // meta.pageCount: number | undefined  (pages/slides/sheets when meaningful)
  }}
  onPageChange={(page, total) => {}}
  onError={(err) => {}}
/>

<DocViewer /> automatically cancels an in-flight render and tears down resources when the source changes or the component unmounts — no manual cleanup needed.


Core API (framework-agnostic)

import {
  renderDocument,
  detect,
  detectFromExtension,
  detectFromBytes,
  normalizeSource,
  setPdfWorkerSrc,
  UnsupportedFormatError,
  FormatDetectionError,
} from 'omni-doc-viewer'

renderDocument(options)

Detects the format, lazy-loads the matching engine, renders into container, and returns a handle.

function renderDocument(options: RenderOptions): Promise<RenderResult>

| Option | Type | Notes | |---|---|---| | container | HTMLElement | required — its contents are replaced. | | source | DocSource | required — URL, File, Blob, ArrayBuffer, Uint8Array. | | type | DocType | Optional override; skips detection. | | signal | AbortSignal | Cancel an in-flight render (details). | | onError | (error: Error) => void | Called on failure; the promise also rejects. | | pdf | { scale?, workerSrc?, textLayer? } | PDF tuning. | | pptx | { width?, height? } | Slide size. |

const view = await renderDocument({
  container: document.getElementById('viewer')!,
  source: file,
  pdf: { scale: 2 },
})

renderDocument runs in the browser only. On the server it throws a clear error — defer to the client (see Next.js / SSR).

RenderResult

interface RenderResult {
  type: DocType
  meta: RenderMeta            // { type, pageCount? }
  pages?: HTMLElement[]       // page/slide/section elements, in order
  destroy(): void             // free listeners, engine resources, clear container
}

Always call destroy() when you're done (the React wrapper does this for you):

const view = await renderDocument({ container, source })
// …
view.destroy()

Format detection

Detection runs in priority order: explicit override → filename/URL extension → magic bytes.

// Full resolver (throws on unsupported/undetectable):
detect({ bytes, filename?, override? }): DocType

// Lower-level helpers (return undefined instead of throwing):
detectFromExtension(nameOrUrl: string): DocType | undefined
detectFromBytes(bytes: Uint8Array): DocType | undefined
detectFromExtension('report.PDF?token=1#p2') // → 'pdf'
detectFromExtension('data.csv')              // → 'csv'

detectFromBytes(pdfBytes)                     // → 'pdf'  (via "%PDF")
detectFromBytes(new Uint8Array([0x89,0x50]))  // → 'image' (PNG)
  • OOXML files (.docx/.xlsx/.pptx) are all ZIPs — detection peeks at the internal part names (word/, xl/, ppt/) to tell them apart without unzipping.
  • Anything that isn't a known binary but looks like UTF-8 text falls back to 'text'.
  • Legacy .doc/.ppt throw UnsupportedFormatError; truly unknown input throws FormatDetectionError.

normalizeSource(source)

Turn any DocSource into raw bytes (+ a filename hint when available). Useful if you want to inspect or detect before rendering.

const { bytes, filename } = await normalizeSource(source, signal?)
const type = detect({ bytes, filename })

Configuration & per-format tuning

Pass pdf and/or pptx to either renderDocument or <DocViewer />.

interface RenderTuning {
  pdf?: {
    scale?: number       // canvas render scale. Default 1.5 (crisp on most displays)
    workerSrc?: string   // explicit pdf.js worker URL (see worker section)
    textLayer?: boolean  // selectable/searchable text overlay. Default true
  }
  pptx?: {
    width?: number       // slide width in px. Default: container width (or 960)
    height?: number      // slide height in px. Default: width * 9/16
  }
}

| Option | Default | When to change it | |---|---|---| | pdf.scale | 1.5 | Raise (e.g. 23) for sharper text/zoom; lower to save memory on huge docs. | | pdf.textLayer | true | Set false for a pure-canvas, image-only render (slightly faster, not selectable). | | pdf.workerSrc | auto | Set if your bundler can't resolve the worker — see below. | | pptx.width / pptx.height | container / 16:9 | Force an exact slide size. |

<DocViewer source={file} pdf={{ scale: 2, textLayer: true }} />
<DocViewer source={deck} pptx={{ width: 1280, height: 720 }} />

You can also set the pdf.js worker globally, once, before rendering any PDF:

import { setPdfWorkerSrc } from 'omni-doc-viewer'
setPdfWorkerSrc('/pdf.worker.min.mjs')

Cancellation & timeouts

renderDocument accepts a standard AbortSignal. Aborting rejects the promise with a DOMException whose name is 'AbortError'.

Cancel manually (e.g. the user navigated away):

const controller = new AbortController()

renderDocument({ container, source, signal: controller.signal })
  .catch((err) => {
    if (err.name === 'AbortError') return // expected — ignore
    throw err
  })

// somewhere else:
controller.abort()

Add a timeout — there's no timeout option; compose one from a signal. Modern environments have AbortSignal.timeout:

await renderDocument({ container, source, signal: AbortSignal.timeout(10_000) })

Or combine a manual abort with a timeout:

const controller = new AbortController()
const t = setTimeout(() => controller.abort(), 10_000)
try {
  const view = await renderDocument({ container, source, signal: controller.signal })
} finally {
  clearTimeout(t)
}

<DocViewer /> manages cancellation automatically — it aborts the previous render whenever source changes and on unmount.


Error handling

renderDocument rejects and calls onError. <DocViewer /> calls onError and shows errorFallback.

import { UnsupportedFormatError, FormatDetectionError } from 'omni-doc-viewer'

try {
  await renderDocument({ container, source })
} catch (err) {
  if (err instanceof UnsupportedFormatError) {
    // legacy .doc/.ppt — inspect err.detectedFormat ('doc' | 'ppt' | undefined)
  } else if (err instanceof FormatDetectionError) {
    // couldn't determine the format — ask the user, or pass `type`
  } else if (err.name === 'AbortError') {
    // render was cancelled — usually safe to ignore
  } else {
    // fetch failure, corrupt file, engine error, …
  }
}

| Error | Thrown when | How to recover | |---|---|---| | UnsupportedFormatError | Legacy binary .doc / .ppt. | Convert to OOXML/PDF server-side, or tell the user. | | FormatDetectionError | Format can't be determined from bytes/extension. | Pass an explicit type, or surface a message. | | DOMException (name: 'AbortError') | The render was cancelled via signal. | Ignore. | | Error (generic) | Network/fetch failure, corrupt file, or engine error. | Retry / show the message. |

In React:

<DocViewer
  source={file}
  errorFallback={(err) =>
    err instanceof UnsupportedFormatError
      ? <p>Legacy Office files aren't supported — please upload a .docx/.pptx or PDF.</p>
      : <p>Couldn't display this document: {err.message}</p>
  }
/>

The pdf.js worker (bundler setup)

PDF rendering uses a pdf.js Web Worker. By default the library resolves the worker bundled with pdfjs-dist via new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url) — which modern bundlers (Vite, webpack 5, Next.js, Parcel) handle automatically. You usually don't need to do anything.

If your bundler can't resolve it, point at a copy you host yourself:

// Option A — globally, once, before any PDF renders:
import { setPdfWorkerSrc } from 'omni-doc-viewer'
setPdfWorkerSrc('/pdf.worker.min.mjs')

// Option B — per render:
renderDocument({ container, source, pdf: { workerSrc: '/pdf.worker.min.mjs' } })
// Option C — per <DocViewer />:
<DocViewer source={file} pdf={{ workerSrc: '/pdf.worker.min.mjs' }} />

Copy node_modules/pdfjs-dist/build/pdf.worker.min.mjs into your public//static dir (or serve it from a CDN) and reference that path.


Framework integration

The core is framework-agnostic: give it a DOM element and a source.

// Vanilla
const view = await renderDocument({ container: el, source })

// Vue 3 (in onMounted, with a template ref)
onMounted(async () => { view = await renderDocument({ container: elRef.value, source }) })
onUnmounted(() => view?.destroy())

// Svelte (in onMount)
onMount(async () => { view = await renderDocument({ container: el, source }); return () => view.destroy() })

For React, prefer the wrapper:

import { DocViewer } from 'omni-doc-viewer/react'

Next.js / SSR

<DocViewer /> is SSR-safe (rendering is deferred to a client effect). In the App Router, use it in a Client Component. If your bundler/runtime hits an SSR edge, load it client-only:

'use client'
import dynamic from 'next/dynamic'

const DocViewer = dynamic(
  () => import('omni-doc-viewer/react').then((m) => m.DocViewer),
  { ssr: false },
)

Bundle size & lazy loading

The package is built around dynamic imports, so you only pay for the formats you actually render:

  • The core entry (renderDocument + detection) is tiny.
  • Each engine — pdf.js, docx-preview, SheetJS, pptx-preview — is imported the first time that format is rendered. Show only PDFs, and SheetJS/PowerPoint code never reaches the user.
  • The heaviest extras load on demand, conditionally:
    • emf-converter — only when a PPTX actually contains an EMF/WMF metafile.
    • marked + dompurify — only when a Markdown file is opened.
    • jszip — only for PPTX media extraction.

"sideEffects": false lets bundlers tree-shake unused exports.


Security

  • Everything is client-side. Files are never uploaded; nothing leaves the browser.
  • Markdown is sanitized. Untrusted Markdown can contain raw HTML; the generated HTML is run through DOMPurify before it touches the DOM, stripping <script>, inline event handlers (onerror, …), javascript:/vbscript: URLs and <iframe>s.
  • No eval, no remote code. Rendering engines parse bytes; they don't execute document scripts.
  • Undecodable/missing images fail closed — e.g. a PPTX metafile that can't be rasterized is hidden rather than rendered as a broken element.

Accessibility

  • PDF text layer (on by default) makes PDFs selectable, searchable, and readable by screen readers — not just an image of a page.
  • Each PDF page is a labelled region (role="region", aria-label="Page N").
  • Toolbar controls have aria-labels and are keyboard-operable; the viewport is focusable and responds to arrow/Page/Home/End keys.
  • The error state uses role="alert".

Browser support

Targets modern evergreen browsers — Chrome / Edge, Firefox, Safari (and Chromium-based mobile browsers). It relies on widely-available web platform features:

  • ES2020, dynamic import(), fetch, <canvas>, Blob/URL.createObjectURL.
  • IntersectionObserver and ResizeObserver power virtualization and the text layer; if absent, the library degrades gracefully (e.g. PDFs render eagerly).
  • The toolbar uses CSS container queries with a @media fallback for older engines.

There is no IE11 support.


TypeScript

Types ship with the package — no @types/* needed.

import type {
  DocType,        // 'pdf' | 'docx' | 'xlsx' | 'pptx' | 'image' | 'text' | 'markdown' | 'csv'
  DocSource,      // string | File | Blob | ArrayBuffer | Uint8Array
  RenderOptions,
  RenderResult,
  RenderMeta,
  RenderTuning,
} from 'omni-doc-viewer'

import type { DocViewerProps, ViewMode } from 'omni-doc-viewer/react'

Comparison

| | omni-doc-viewer | Office Online iframe | Per-format libraries | |---|---|---|---| | Works offline / on local files | ✅ | ❌ (needs internet + public URL) | ➖ varies | | One API for all formats | ✅ | ➖ | ❌ wire each yourself | | No server / no API keys | ✅ | ❌ | ✅ | | Selectable PDF text | ✅ | ✅ | ➖ | | Large-doc virtualization | ✅ | ✅ | ❌ usually | | EMF/WMF + placeholder images in PPTX | ✅ | ✅ | ❌ usually | | Lazy per-format loading | ✅ | n/a | ➖ |


FAQ

Does it upload my files anywhere? No. Everything runs in the browser; bytes never leave the page.

Can it render a PDF from a Uint8Array / decrypted buffer? Yes — pass the bytes as source. Add type: 'pdf' if there's no filename.

Why is my PDF text not selectable? Make sure pdf.textLayer isn't set to false. Note that scanned/image-only PDFs have no text to select.

Can I print or download from code? The pagination toolbar has built-in download & print buttons. For programmatic download, you already hold the source — use a normal anchor/URL.createObjectURL.

It says "renders in the browser only" on my server. That's expected during SSR — defer rendering to the client (see Next.js / SSR).

How big is it? The core is small; engines load on demand. You only ship the formats you actually render — see Bundle size & lazy loading.


Contributing

Contributions are welcome! This is a pnpm monorepo (library + Vite demo).

pnpm install
pnpm dev:demo       # Vite playground — drag-drop any format, works offline
pnpm build          # ESM + CJS + .d.ts for "." and "./react"
pnpm test           # vitest
pnpm typecheck      # tsc --noEmit
pnpm lint           # eslint
pnpm verify         # lint + typecheck + test (run before a PR)
pnpm samples        # regenerate the demo sample documents
  • Commits follow Conventional Commits (enforced by commitlint): feat:, fix:, docs:, chore:, …
  • Run pnpm verify before opening a pull request.
  • Found a bug or have a feature request? Open an issue.

License

MIT © akbhuker.

The bundled rendering engines keep their own licenses — see NOTICE.