viewdoc
v0.4.0
Published
Universal React document viewer — images, PDFs, Word and Excel files with zoom, pan and scroll.
Maintainers
Readme
viewdoc
Universal React document viewer — images, PDFs, Word documents and Excel spreadsheets, all with zoom, pan, fullscreen and a consistent toolbar.
import { DocViewer } from 'viewdoc'
import 'viewdoc/styles.css'
<DocViewer uri="https://example.com/report.pdf" />That's it — DocViewer detects the file type from the URL and renders the right viewer. It loads whichever parser it needs (PDF/DOCX/XLSX) lazily at runtime — importing viewdoc for image-only use costs ~70KB gzipped, not the full bundle.
Install
npm install viewdocreact and react-dom (17+) are peer dependencies — install them if your project doesn't already have them.
Don't forget the stylesheet:
import 'viewdoc/styles.css'Next.js App Router: every component ships with a "use client" directive, so you can import and render DocViewer directly from a Server Component without wrapping it yourself.
Supported file types
| Type | Extensions | Powered by |
|---|---|---|
| Images | .png .jpg/.jpeg .gif .webp .svg .bmp .ico .avif | native <img> |
| PDF | .pdf | pdfjs-dist |
| Word | .docx | mammoth |
| Excel | .xlsx .xls (legacy binary) .xlsm .csv | xlsx (SheetJS) |
Not supported: old binary .doc (pre-2007 Word) — there's no reliable way to parse it in the browser. TIFF/HEIC/RAW images also aren't supported since browsers can't decode them natively.
Components
DocViewer (recommended)
Auto-detects the file type and renders the matching viewer.
<DocViewer uri="https://example.com/invoice.xlsx" fileName="invoice.xlsx" />Detection reads the URL's path (ignoring query strings, so presigned S3/CloudFront URLs work correctly). If your URL has no reliable extension (e.g. an opaque storage key), force it:
<DocViewer uri="https://example.com/blob/9f8a2b" type="pdf" />type also accepts a MIME type ("application/pdf", "image/png", etc.) if that's what you have on hand.
If detection fails and no type is given, DocViewer renders a fallback node (or a small default message):
<DocViewer uri={file.url} fallback={<p>Can't preview this file type.</p>} />Individual viewers
ImageViewer is cheap (no extra dependencies) and lives on the main entry. PdfViewer, DocxViewer and XlsxViewer each pull in a sizeable parser (pdfjs-dist, mammoth, xlsx respectively), so they live on their own subpaths — this keeps viewdoc's main entry small for apps that only need images, and DocViewer loads whichever one it needs lazily at runtime instead of bundling all three upfront.
import { ImageViewer } from 'viewdoc'
import { PdfViewer } from 'viewdoc/pdf'
import { DocxViewer } from 'viewdoc/docx'
import { XlsxViewer } from 'viewdoc/xlsx'
<ImageViewer uri="https://example.com/photo.jpg" />
<PdfViewer uri="https://example.com/report.pdf" />
<DocxViewer uri="https://example.com/letter.docx" />
<XlsxViewer uri="https://example.com/budget.xlsx" />Props
DocViewer and all four viewers share the same prop shape (DocViewer just adds type and fallback on top):
| Prop | Type | Default | Description |
|---|---|---|---|
| uri | string | required | URL or data URI of the file |
| fileName | string | — | Alt text / suggested download filename |
| className / style | — | — | Applied to the outer wrapper |
| width / height | number \| string | 1200 / 700 | Viewer size (px number or any CSS size) |
| minScale / maxScale / zoomStep | number | varies by viewer | Zoom range and increment |
| enableZoomControls | boolean | true | Show zoom out/in/reset buttons |
| enableWheelZoom | boolean | true | Ctrl/Cmd + scroll wheel to zoom |
| enablePan (ImageViewer only) | boolean | true | Click-drag-release panning of the image |
| enableFullscreen | boolean | true | Show fullscreen toggle |
| enableDownload | boolean | true | Show download button |
| enableIframeFallback (PdfViewer only) | boolean | true | Fall back to a plain <iframe> when the PDF can't be fetched (usually a CORS-blocked server) instead of showing an error |
| onDownload | () => void | — | Custom download handler; defaults to downloading uri |
| theme | ViewerTheme | — | Override colors/radii (see below) |
| floating | boolean | true | Render as a centered, draggable floating window vs. an embedded block that fills the parent |
| windowDraggable | boolean | true | When floating, allow dragging via the toolbar's grip icon |
| defaultPosition | { x: number; y: number } | centered in viewport | Initial position when floating |
Every boolean defaults to true — pass false to turn a feature off. Nothing needs to be configured to get a fully working viewer.
Theming
Pass a theme object to override the default dark look. Any field you omit keeps its default:
<DocViewer
uri={file.url}
theme={{
background: '#0b1220',
toolbarBackground: '#111827',
toolbarBorderColor: '#1f2937',
toolbarButtonHoverBackground: '#1f2937',
textColor: '#93c5fd',
borderRadius: '16px',
}}
/>Available fields: background, toolbarBackground, toolbarBorderColor, toolbarButtonHoverBackground, textColor, borderRadius, toolbarButtonRadius.
Embedded vs. floating mode
By default every viewer opens as a floating window: centered on screen, draggable by its toolbar grip icon, sized 1200×700. This is meant for "preview this file" use cases (e.g. clicking a file in a list).
If you want the viewer to sit inline in your layout instead — filling whatever container you put it in — set floating={false}:
<div style={{ width: 600, height: 400 }}>
<DocViewer uri={file.url} floating={false} />
</div>In non-floating mode, the viewer still renders at its width/height (default 1200×700) and centers itself inside the parent container.
Interaction reference
- Zoom:
+/−buttons, click the%label or the reset icon to return to 100%, hold Ctrl/Cmd and scroll, or pinch with two fingers on touch devices - Pan: click-drag the content directly (images), or click-drag/scroll anywhere on the canvas (PDF, DOCX, XLSX — these use native scrolling under the hood, so the mouse wheel always works too)
- Move the window: click-drag the small grip icon (⊹) at the left of the toolbar (only shown when
floatingistrue) - Fullscreen: toggle button on the right of the toolbar
- Download: button next to fullscreen; downloads
uriunless you passonDownload - Keyboard: every toolbar button is reachable via Tab and activates with Enter/Space; Tab once more to focus the document content, then use arrow keys to pan (images) or arrow keys/Page Up/Down/Space (PDF, DOCX, XLSX — native browser scroll on the focused container)
When a PDF's server doesn't allow CORS
PdfViewer needs to fetch() the raw PDF bytes in JavaScript to parse and render them (that's what gives you the shared toolbar, zoom, and virtualization) — this only works if the server hosting the file sends the right CORS headers. This is the same requirement every canvas-based PDF viewer has (not specific to viewdoc); a plain <iframe src="..."> works without CORS because it's just navigation, not a JS-readable fetch.
If the file's server doesn't send CORS headers (common with third-party links, or your own storage before you've configured it), PdfViewer automatically falls back to displaying the PDF via a plain <iframe> using the browser's native PDF viewer — you lose the custom toolbar/zoom/virtualization for that file, but the user still sees the document instead of an error. Disable this with enableIframeFallback={false} if you'd rather show the error message instead.
For files on your own storage (S3, GCS, your own server, etc.), the better fix is to configure CORS on that storage once — it then works with the full PdfViewer experience for every file, permanently. For S3, add this to the bucket's Permissions → CORS configuration:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["*"],
"ExposeHeaders": ["Content-Length", "Content-Range", "ETag"],
"MaxAgeSeconds": 3600
}
]Replace "AllowedOrigins": ["*"] with your app's actual domain for tighter security.
Known limitations
- Old binary
.doc/legacy Word formats before 2007 aren't supported (only.docx) - The Excel table view is read-only — no cell editing or selection
XlsxViewerfalls back to CSV parsing for unrecognized content (this is intentional — SheetJS supports.csv), so pointing it at a non-spreadsheet file won't always error; it may render the raw text as a single cell insteadDocxViewerandXlsxViewerrender content viadangerouslySetInnerHTMLusing HTML generated from the file itself (bymammoth/xlsx); that HTML is passed through DOMPurify before rendering to strip scripts/event handlers/dangerous URI schemes (e.g. ajavascript:hyperlink target embedded in a malicious file)PdfViewerloads its PDF.js worker script from a CDN (jsDelivr) pinned to the exactpdfjs-distversion bundled withviewdoc, rather than resolving it as a local module path. This avoids an "API version does not match the Worker version" error that happens when your app also depends onpdfjs-distthrough another library (e.g.react-pdf) at a different version — bundlers commonly fail to disambiguate the two nested copies and load the wrong worker. The tradeoff is thatPdfViewerrequires network access to jsdelivr.net; if that's blocked (e.g. an offline app or a strict CSP), setpdfjsLib.GlobalWorkerOptions.workerSrcyourself to a self-hosted copy before rendering anyPdfViewerPdfVieweronly renders real canvases for pages currently visible (adapting to zoom level, so zooming out to see many pages at once doesn't leave any of them stuck as blank placeholders), plus a ±3 page buffer; other pages show a lightweight sized placeholder, so memory and initial render time stay low regardless of document lengthXlsxViewercaps rendered rows atmaxRows(default 2000) per sheet, since the table has no virtualization either — a 10,000-row sheet took ~9 seconds to render unbounded vs. ~1.7 seconds capped. RaisemaxRowsif you need more and can accept the tradeoff, or download the file for the full data- The Fullscreen API isn't supported on iOS Safari for anything other than
<video>elements — the fullscreen button is automatically hidden there rather than showing a control that can't work - The
xlsxdependency is installed from SheetJS's own CDN rather than the npm registry — SheetJS stopped publishing security-patched releases to npm after0.18.5, which has known unpatched CVEs (prototype pollution, ReDoS). This is SheetJS's own officially documented install method for getting patched versions
License
MIT
