Skip to main content

DocumentViewer

DocumentViewer

Show PDF, Word, Excel, CSV and CAD files in the browser, with format detection and lazy-loaded viewers

Tags: viewer document pdf docx xlsx csv dxf dwg file preview

DocumentViewer shows a file inline: it loads the bytes, works out the format from the file's content, and lazy-loads the viewer you registered for it. Everything happens in the browser. Nothing is uploaded or converted on a server, so it works offline and on Cloudflare Workers apps.

View only: navigate, zoom, find, print and download. Svelte only for now; a React package is planned. Try it on the document viewers demo.

Format Viewer Import Peer dependency
PDF PdfViewer @classic-homes/viewer-svelte/pdf pdfjs-dist@^6
Excel, CSV SpreadsheetViewer @classic-homes/viewer-svelte/xlsx none
Word DocxViewer @classic-homes/viewer-svelte/docx docx-preview
DXF (CAD) CadViewer @classic-homes/viewer-svelte/cad dxf-viewer
DWG (CAD) DwgViewer @classic-homes/viewer-dwg/svelte GPL-3.0 package

Import​

<script>
  import { DocumentViewer } from '@classic-homes/viewer-svelte';
</script>

Setup​

npm install @classic-homes/viewer-svelte pdfjs-dist docx-preview dxf-viewer

Install only the peers for the formats you show. Then configure the libraries that need it once, at app start (e.g. src/hooks.client.ts):

import { configurePdf } from '@classic-homes/viewer-svelte/pdf';
import pdfWorkerSrc from 'pdfjs-dist/build/pdf.worker.min.mjs?url';
import { configureSpreadsheet } from '@classic-homes/viewer-svelte/xlsx';
import SheetWorker from '@classic-homes/viewer-core/sheet/worker?worker';
import { configureCad } from '@classic-homes/viewer-svelte/cad';
import CadWorker from '@classic-homes/viewer-svelte/cad/worker?worker';

configurePdf({ workerSrc: pdfWorkerSrc });
configureSpreadsheet({ worker: () => new SheetWorker() });
configureCad({ worker: () => new CadWorker(), fonts: ['/fonts/NotoSans-Regular.ttf'] });

Add the package to your Tailwind content, so its classes are generated:

'./node_modules/@classic-homes/viewer-svelte/dist/**/*.{js,svelte}',

Basic Usage​

<script lang="ts">
  import { DocumentViewer, acceptFor, type ViewerRegistry } from '@classic-homes/viewer-svelte';

  // Each entry is a lazy import: formats you leave out are never bundled
  const viewers: ViewerRegistry = {
    pdf: () => import('@classic-homes/viewer-svelte/pdf'),
    docx: () => import('@classic-homes/viewer-svelte/docx'),
    xlsx: () => import('@classic-homes/viewer-svelte/xlsx'),
    csv: () => import('@classic-homes/viewer-svelte/xlsx'),
    dxf: () => import('@classic-homes/viewer-svelte/cad'),
  };

  let file = $state<File>();
</script>

<input type="file" accept={acceptFor()} onchange={(e) => (file = e.currentTarget.files?.[0])} />
<DocumentViewer source={file} {viewers} class="h-[80vh]" />

source accepts a File or Blob, a URL (string or URL, fetched with fetchInit), an ArrayBuffer or typed array, or an offline-core stored file ({ blob, filename, contentType }).

Props​

Prop Type Default Description
source ViewerSource - File, Blob, URL, bytes or offline-core stored file. Changing it loads the new one
viewers ViewerRegistry required Viewers to use, as lazy imports keyed by format
name string - File name hint (extension fallback, download name) when the source has none
mime string - MIME type hint when the source has none
maxBytes number 104857600 Size cap in bytes (100 MB); larger files get a "too large" state
fetchInit RequestInit - fetch options for URL sources (credentials, auth headers)
allowDownload boolean true Offer "Download file" when a file cannot be shown
hideToolbar boolean false Hide the viewer toolbar
onload (info: DocumentLoadInfo) => void - The document is on screen: { kind, name, size }
onerror (error: ViewerError) => void - Loading or rendering failed
class string - Classes for the outer element; give it a height

Detection and errors​

The format comes from the file's content first (magic bytes), then its extension, then its MIME type, so a mislabelled file still opens. Files the viewer cannot show get a state that explains why, with a download button:

ViewerError code When
unsupported No viewer registered for the format, legacy .doc/.xls, binary DXF, no WebGL
too-large Bigger than maxBytes
encrypted Password-protected PDF or Office file
corrupt The parser rejected the file
load-failed Network or read failure
missing-dependency The format's peer library is not installed

acceptFor(kinds?) builds a file input's accept value. Pass your registry's keys (Object.keys(viewers)) to offer exactly what you can show.

Keyboard​

All viewers share these shortcuts while focus is inside them:

Keys Action
Ctrl/Cmd + + / - Zoom in / out
Ctrl/Cmd + 0 Reset zoom: PDF toggles fit width/page; Word fit width/100%; Excel 100%; CAD fit
Ctrl/Cmd + F Find (PDF, Word, Excel)
Enter / Shift+Enter Next / previous match
PageDown / PageUp Next / previous page
Home / End First / last page

Security​

Documents are untrusted input. Office files are ZIP archives: each entry's declared size is checked before it is inflated, so a small file cannot expand into gigabytes. Links found in documents only open when they are http(s) or mailto, in a new tab without opener access. pdf.js runs without eval. Word documents render detached, with embedded HTML switched off, and are sanitised (including CSS @import and remote url()) before they are attached to a shadow root. As defence in depth, serve pages that show untrusted documents with a Content Security Policy whose style-src, img-src and font-src list no remote origins (see DocxViewer).

On this page