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 |
|---|---|---|---|
| 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-viewerInstall 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).
Related
- PdfViewer: PDF pages with text selection and find
- SpreadsheetViewer: Excel and CSV grids
- DocxViewer: Word documents as pages
- CadViewer: DXF drawings with layers
- DwgViewer: DWG drawings (GPL, opt-in)