A framework-agnostic embedded file viewer for vanilla JavaScript, React, Vue and Svelte. Put PDF, Office, images, media, archives, email, drawings, 3D, GIS and engineering files inside one stable container.
See the code
English | Simplified Chinese | 日本語 | 한국어 | Español | Português
Open File Viewer is a file preview SDK for modern web applications. It brings PDFs, Office documents, images, audio and video, archives, emails, drawings, 3D files, GIS data, and source code into one controlled container, with support for vanilla JavaScript, React, Vue, and Svelte.
Website | About | GitHub | NPM Core | React | Vue | Svelte
Most business systems eventually need attachment preview: contracts, spreadsheets, drawings, archives, emails, images, videos, and source files. Open File Viewer is not a PDF-only demo. It is a file preview foundation that can evolve with real product needs over time.
px, %, vh, vw, rem, and calc(), and responds automatically to container changes.pnpm add @open-file-viewer/core
React:
pnpm add @open-file-viewer/core @open-file-viewer/react
Vue:
pnpm add @open-file-viewer/core @open-file-viewer/vue
Svelte:
pnpm add @open-file-viewer/core @open-file-viewer/svelte
PDF preview requires pdfjs-dist when you use pdfPlugin():
pnpm add pdfjs-dist
Angular CLI 20 uses Vite to pre-bundle dependencies. If an application only needs image and PDF preview, use the lightweight entry so the optimizer never enters the Office conversion dependency graph:
import {
createViewer,
fallbackPlugin,
imagePlugin,
pdfPlugin
} from "@open-file-viewer/core/lite";
import "@open-file-viewer/core/style.css";
The lite entry deliberately contains only the viewer runtime, image, PDF, and fallback plugins.
Use the main @open-file-viewer/core entry when the application needs Office, CAD, archive, or
other formats. Do not mix the two entries in the same application.
When using officePlugin() with Vite, exclude the converter from dependency pre-bundling and
externalize its optional Node canvas fallback from the browser build. EMF/WMF conversion in the
browser still uses Canvas:
// vite.config.ts — merge these options into your existing config
import { defineConfig } from "vite";
export default defineConfig({
optimizeDeps: { exclude: ["emf-converter"] },
build: { rollupOptions: { external: ["@napi-rs/canvas"] } }
});
You can also use npm or yarn:
npm install @open-file-viewer/core
yarn add @open-file-viewer/core
Import the shared stylesheet once in your application:
import "@open-file-viewer/core/style.css";
import {
createViewer,
imagePlugin,
videoPlugin,
audioPlugin,
textPlugin,
pdfPlugin,
officePlugin,
archivePlugin,
emailPlugin,
drawingPlugin,
xmindPlugin,
cadPlugin,
model3dPlugin,
gisPlugin,
fallbackPlugin
} from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
const plugins = [
imagePlugin(),
videoPlugin(),
audioPlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin(),
archivePlugin(),
emailPlugin(),
drawingPlugin(),
xmindPlugin(),
cadPlugin(),
model3dPlugin(),
gisPlugin(),
fallbackPlugin()
];
const viewer = createViewer({
container: "#viewer",
file: fileOrUrl,
fileName: "contract.pdf",
width: "100%",
height: "70vh",
fit: "contain",
toolbar: true,
theme: "auto",
plugins
});
viewer.resize();
viewer.destroy();
Use the same plugin list to check support before mounting a viewer:
import { isPreviewSupported } from "@open-file-viewer/core";
const supported = await isPreviewSupported(fileOrUrl, plugins, {
fileName: "contract.pdf",
mimeType: "application/pdf"
});
The check normalizes the source exactly like createViewer() and evaluates
plugin.match() in order. It does not mount DOM or call plugin.render(), and
fallbackPlugin() is not counted as native preview support.
Keep the plugin order identical to the viewer configuration; a matching
fallbackPlugin() is terminal and must not be placed before native plugins.
When PDF.js cannot parse or load a remote PDF URL, the PDF plugin falls back to
the browser's embedded PDF viewer. Different Chromium-based viewers apply
different script requirements inside sandboxed iframes, so
webFallbackScripts: "auto" is the default:
allow-scripts while remaining sandboxed;The host can make the policy stricter, or explicitly trust a same-origin PDF endpoint:
pdfPlugin({ webFallbackScripts: "never" }); // strict sandbox
pdfPlugin({ webFallbackScripts: "always" }); // trusted PDF endpoints only
This setting only affects the remote iframe fallback. It does not change the
normal PDF.js rendering path or force a referrerpolicy value.
PDF.js 5+ uses files from pdfjs-dist/wasm/ to decode formats such as JPEG2000
(JPX) and JBIG2. Serve that directory from your application and pass its public
URL, including the trailing slash, to the plugin:
pdfPlugin({
workerSrc: pdfWorkerSrc,
wasmUrl: "/pdfjs/wasm/"
});
Keep the hosted WASM files on the same version as the installed pdfjs-dist.
PDF.js 4 ignores this forward-compatible option.
Micro-frontend sandboxes (qiankun, micro-app, ...) tear down window message
listeners when a sub-app unmounts, which breaks the setImmediate polyfill that
jszip depends on. JSZip.loadAsync then never resolves, so every zip-based
preview (docx / xlsx / pptx / epub / ofd) hangs on the loading state forever
(see qiankun#2589).
createViewer() detects sandbox flags such as __POWERED_BY_QIANKUN__ and
__MICRO_APP_ENVIRONMENT__ and automatically installs a MessageChannel-based
scheduler that sandboxes cannot break — no configuration needed. On versions up
to 0.1.27, patch it manually in the sub-app entry before any imports:
if (window.__POWERED_BY_QIANKUN__) {
window.setImmediate = (fn, ...args) => setTimeout(fn, 0, ...args);
}
import { FileViewer } from "@open-file-viewer/react";
import { imagePlugin, pdfPlugin, officePlugin, textPlugin } from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
const plugins = [
imagePlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin()
];
export function AttachmentPreview({ file }: { file: File }) {
return (
<FileViewer
file={file}
fileName={file.name}
width="100%"
height="640px"
fit="contain"
toolbar
theme="auto"
plugins={plugins}
/>
);
}
<script setup lang="ts">
import { OpenFileViewer } from "@open-file-viewer/vue";
import { imagePlugin, pdfPlugin, officePlugin, textPlugin } from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
defineProps<{ file: File }>();
const plugins = [
imagePlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin()
];
</script>
<template>
<OpenFileViewer
:file="file"
:file-name="file.name"
width="100%"
height="640px"
fit="contain"
toolbar
theme="auto"
:plugins="plugins"
/>
</template>
<script lang="ts">
import { OpenFileViewer } from "@open-file-viewer/svelte";
import { imagePlugin, pdfPlugin, officePlugin, textPlugin } from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
export let file: File;
const plugins = [
imagePlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin()
];
</script>
<OpenFileViewer
{file}
fileName={file.name}
width="100%"
height="640px"
fit="contain"
toolbar
theme="auto"
{plugins}
/>
| Scenario | What Open File Viewer Provides |
|---|---|
| OA / ERP / CRM attachment centers | A unified container preview for contracts, spreadsheets, images, emails, and archives |
| Cloud drives / knowledge bases / document systems | Multi-file queues, download, search, fullscreen, and theme adaptation |
| Low-code / form systems | Vanilla JS integration without forcing React, Vue, or Svelte |
| Engineering / manufacturing / GIS systems | Recognition and progressive enhancement for CAD, 3D, GIS, and drawing files |
| Developer platforms / log platforms | Text, config, Markdown, code highlighting, and large-file protection |
| Capability | Status |
|---|---|
| Vanilla JS / React / Vue / Svelte integration | Supported |
| Custom container, width, height, and responsive sizing | Supported |
| Multi-file queue, switching, and current index | Supported |
| Toolbar, download, fullscreen, print, and search | Supported |
Light, dark, and auto themes | Supported |
Local File / Blob / URL / ArrayBuffer sources | Supported |
| Plugin protocol and custom fallback | Supported |
| PDF, images, audio/video, text/code | Supported |
| Office, OFD, EPUB, XPS, email, and archives | Basic to enhanced preview |
| CAD, 3D, GIS, drawing boards, and design assets | Detection, basic preview, and ongoing enhancements |
| Category | Plugin | Representative Formats |
|---|---|---|
| Images | imagePlugin() | jpg, png, gif, webp, avif, svg, bmp, tiff, heic, heif |
| Video | videoPlugin() | mp4, webm, mov, m4v, avi, mkv, flv, wmv, m3u8, m2ts |
| Audio | audioPlugin() | mp3, wav, ogg, aac, m4a, flac, opus, mid, wma |
| Text / code | textPlugin() | txt, lrc, md, json, yaml, xml, csv, js, ts, tsx, vue, html, css, py, go, rs, sql, sh |
| PDF / ebooks | pdfPlugin(), epubPlugin(), xpsPlugin() | pdf, epub, xps, oxps |
| Office | officePlugin() | doc, docx, docm, dot, rtf, odt, xls, xlsx, xlsm, xlsb, csv, ppt, pps, pptx, pptm, odp, wps, et, dps |
| OFD | ofdPlugin() | ofd |
| Archives | archivePlugin() | zip, rar, 7z, tar, gz, tgz, bz2, xz |
emailPlugin() | eml, msg, mbox | |
| Drawing / whiteboard | drawingPlugin() | drawio, dio, excalidraw, tldraw |
| Mind maps | xmindPlugin() | xmind |
| CAD / engineering / chip layout | cadPlugin() | dxf, dwg, dwf, step, stp, iges, igs, ifc, skp, sldprt, gds, gdsii, oas, oasis |
| 3D models | model3dPlugin() | gltf, glb, obj, stl, fbx, dae, ply, 3mf, usd, usdz |
| GIS | gisPlugin() | geojson, topojson, kml, kmz, gpx, shp |
| Asset recognition | assetPlugin() | ttf, woff2, psd, ai, eps, sqlite, wasm, parquet, avro |
Preview quality for complex formats depends on browser capabilities, file structure, and the parser used by each plugin. The current version focuses on making every format enter a controlled preview path inside the container. High-fidelity Office, CAD, design, and proprietary binary formats can continue to integrate dedicated engines or server-side conversion.
Legacy ppt / pps files use a local OLE and PowerPoint Binary File Format preview path. It restores slide geometry, positioned text, master bitmap artwork, JPEG/PNG/TIFF assets, and common compressed EMF/WMF graphics without uploading the file. Unsupported drawing records fall back gracefully; use officePlugin({ convert }) when pixel-identical Office rendering is required.
Markdown code blocks display line numbers and a copy button in the upper-right corner. Copying preserves the code text and excludes line numbers; button labels and feedback use the existing textCopy, textCopied, and textCopyFailed messages.
Plugin order matters because the first matching plugin renders the file. For example, csv and tsv can match both textPlugin() and officePlugin(); place officePlugin() earlier if you want spreadsheet-style table preview.
DWG is AutoCAD's proprietary binary format. cadPlugin() can use a high-fidelity WebGL scene, the lightweight built-in SVG path, or an application-provided renderer.
webglDwg to parse DWG in a Worker and draw layers, blocks, hatches, line styles, and text in an interactive CAD canvas. Once configured, errors are surfaced instead of silently switching to SVG.cadPlugin() automatically tries LibreDWG WASM for DWG model-space linework. If the linework looks unreliable but the file contains an embedded thumbnail, it shows the DWG thumbnail. If LibreDWG is not installed, the WASM path is not configured, or parsing fails, it falls back to DWG/DWF metadata, version hints, structure probes, and conversion guidance.cadPlugin({ binaryRenderer }) to integrate your own frontend engine, CADViewer, MxCAD, or a backend service that converts to PNG/PDF/SVG/DXF. binaryRenderer has the highest priority and fully takes over DWG/DWF preview when it returns an instance.Recommended high-fidelity setup:
npm install @mlightcad/cad-simple-viewer@1.5.10 @mlightcad/data-model@1.12.5 lodash-es@4.17.21
Copy libredwg-parser-worker.js and mtext-renderer-worker.js from the installed
@mlightcad/cad-simple-viewer/dist/ directory into a public static directory,
then configure that directory:
cadPlugin({
webglDwg: {
engineLoader: () => import("@mlightcad/cad-simple-viewer"),
workerBaseUrl: "/vendor/cad-engine"
}
});
The explicit engineLoader keeps the optional engine out of builds that do not
configure WebGL DWG preview and gives strict esbuild setups a host-owned import
they can resolve.
The WebGL package is MIT licensed. Its published DWG parser Worker is based on
LibreDWG, so applications should also review the Worker's license requirements.
CAD font resources may be configured with webglDwg.baseUrl; only self-host
font files that your application is licensed to distribute.
Lightweight LibreDWG SVG setup:
npm install @mlightcad/libredwg-web@0.7.4
scripts/copy-libredwg-assets.mjs to the host application. This example
targets the conventional public/ directory used by Vite and Next.js; change
targetRoot if your framework uses a different static directory.import { cp, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const moduleEntry = fileURLToPath(import.meta.resolve("@mlightcad/libredwg-web"));
const packageRoot = dirname(dirname(moduleEntry));
const targetRoot = fileURLToPath(
new URL("../public/vendor/libredwg-web/", import.meta.url)
);
await mkdir(targetRoot, { recursive: true });
await Promise.all([
cp(join(packageRoot, "dist"), join(targetRoot, "dist"), {
recursive: true,
force: true
}),
cp(join(packageRoot, "wasm"), join(targetRoot, "wasm"), {
recursive: true,
force: true
})
]);
{
"scripts": {
"assets:dwg": "node scripts/copy-libredwg-assets.mjs",
"predev": "npm run assets:dwg",
"prebuild": "npm run assets:dwg"
}
}
If the project already defines predev or prebuild, append the asset command
to the existing script instead of replacing it.
dist/ and wasm/ directory layout and configure
the browser-loadable ESM entry:cadPlugin({
libreDwg: {
wasmBaseUrl: "/vendor/libredwg-web/wasm",
workerModuleUrl: "/vendor/libredwg-web/dist/libredwg-web.js",
workerTimeoutMs: 120_000
}
});
After starting the application, verify that both URLs return 200:
/vendor/libredwg-web/dist/libredwg-web.js
/vendor/libredwg-web/wasm/libredwg-web.wasm
For applications deployed below a URL prefix, prepend the application's public
base path to both configuration URLs. Serve .wasm as application/wasm.
These are self-hosted static assets, not files loaded from an external CDN.
The documentation app runs doc/scripts/copy-libredwg-assets.mjs during
predev and prebuild, copying the pinned npm package's dist/ and wasm/
directories into doc/public/vendor/libredwg-web/. Those generated directories
are ignored by Git because they are reproducible and include a roughly 6 MB
WASM binary. Host applications should add an equivalent copy step to their own
build or deployment pipeline. If the deployment platform cannot run that step,
publish the copied directories as static assets instead.
@mlightcad/libredwg-web is an optional GPL-3.0 dependency; it is intentionally
not bundled into the MIT-licensed core package. Applications that enable it
should review the upstream license requirements for their distribution model.
The viewer transfers a DWG buffer copy to a dedicated module worker and
terminates it when the file changes, the viewer is destroyed, or parsing times
out. The ESM/WASM assets must be same-origin or CORS-enabled, and CSP must allow
module workers plus blob: in worker-src. Without a worker URL, the existing
main-thread SVG/thumbnail fallback remains active.
cadPlugin({
async binaryRenderer({ panel, extension, arrayBuffer, fileName }) {
if (extension !== "dwg") return;
const stage = document.createElement("div");
stage.className = "my-dwg-stage";
panel.append(stage);
// Load your DWG engine, worker, fonts, and assets on demand here.
// Example: await renderDwgWithYourEngine(stage, arrayBuffer, { fileName });
return {
destroy() {
stage.remove();
}
};
}
});
createViewer(options: PreviewOptions): FileViewer;
| Option | Type | Default | Description |
|---|---|---|---|
container | HTMLElement | string | Required | Preview container |
file | File | Blob | string | ArrayBuffer | - | Single-file preview source |
files | (PreviewSource | PreviewItem)[] | - | Multi-file preview queue |
initialIndex | number | 0 | Initial file index |
initialPage | number | 1 | Initial 1-based page for paginated previews |
fileName | string | Auto inferred | File name used for extension detection |
mimeType | string | Auto inferred | MIME type. An explicitly supplied text MIME type takes priority over the file extension, so text/plain can show Markdown source while text/markdown renders it. |
width | number | string | Original container width | Preview container width |
height | number | string | Original container height | Preview container height |
zoom | number | 1 | Initial zoom level, where 1 means 100% |
fit | contain | cover | width | height | actual | scale-down | contain | Content fitting mode. When omitted for a direct PDF preview, the continuous reader uses width; pass contain explicitly to fit both dimensions. |
plugins | PreviewPlugin[] | [] | Plugin list, matched in order |
fallback | inline | download | custom | inline | Fallback strategy for unsupported formats |
renderFallback | (ctx) => PreviewInstance | - | Custom fallback renderer |
toolbar | boolean | PreviewToolbarOptions | false | Toolbar configuration |
theme | light | dark | auto | light | Viewer theme |
className | string | - | Extra container class name |
onLoad | (file) => void | - | Callback after loading completes |
onError | (error, file) => void | - | Error callback |
onUnsupported | (file) => void | - | Unsupported-format callback |
toolbar: true enables the default toolbar, including multi-file navigation, zoom, rotate, download, fullscreen, print, and search when supported by the active plugin. You can extend it for business workflows without rewriting the whole viewer.
Set download or print to false when those actions are provided outside the viewer or gated by application permissions. When passing a toolbar options object, explicitly enable the other built-in controls you want to retain:
createViewer({
container: "#viewer",
file,
toolbar: {
zoom: true,
rotate: true,
download: permissions.canDownload,
fullscreen: true,
print: permissions.canPrint,
search: true
},
plugins
});
Use download: false and print: false to remove both buttons completely. The viewer only controls their visibility; permission checks for controls rendered elsewhere remain the application's responsibility.
createViewer({
container: "#viewer",
file,
toolbar: {
zoom: true,
rotate: true,
download: true,
fullscreen: true,
search: true,
labels: {
download: "Download",
fullscreen: "Fullscreen",
search: "Search",
"zoom-in": "Zoom in",
"zoom-out": "Zoom out",
"zoom-reset": "Actual size",
"rotate-right": "Rotate"
},
titles: {
download: "Download current file"
},
icons: {
download: '<svg viewBox="0 0 24 24"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"/></svg>'
},
order: ["search", "zoom-out", "zoom-in", "zoom-reset", "rotate-right", "download", "fullscreen"]
},
plugins
});
createViewer({
container: "#viewer",
file,
toolbar: {
order: ["download", "favorite", "approve", "share", "fullscreen"],
actions: [
{
id: "favorite",
label: "Favorite",
onClick(ctx) {
favoriteFile(ctx.file);
}
},
{
id: "approve",
label: "Approve",
onClick(ctx) {
openApprovalDialog(ctx.file);
}
},
{
id: "share",
label: "Share",
disabled(ctx) {
return !ctx.file;
},
onClick(ctx) {
shareFile(ctx.file);
}
}
]
},
plugins
});
createViewer({
container: "#viewer",
files,
toolbar: {
render(ctx) {
const bar = document.createElement("div");
bar.className = "business-toolbar";
const name = document.createElement("strong");
name.textContent = ctx.file?.name || "";
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next";
next.disabled = !ctx.canNext;
next.onclick = () => void ctx.next();
const download = document.createElement("button");
download.type = "button";
download.textContent = "Download";
download.onclick = ctx.download;
bar.append(name, next, download);
return bar;
}
},
plugins
});
The render(ctx) context includes file, index, length, previous(), next(), goToPage(page), command(), download(), fullscreen(), print(), search(), and clearSearch(). In core, toolbar.render(ctx) returns a DOM HTMLElement | void; React, Vue, and Svelte expose framework-native toolbar APIs.
<FileViewer
files={files}
plugins={plugins}
renderToolbar={(ctx) => (
<>
<button disabled={!ctx.canPrevious} onClick={() => void ctx.previous()}>Previous</button>
<span>{ctx.index + 1} / {ctx.length}</span>
<button disabled={!ctx.canNext} onClick={() => void ctx.next()}>Next</button>
<button onClick={ctx.download}>Download</button>
<button onClick={() => openApprovalDialog(ctx.file)}>Approve</button>
</>
)}
/>
<OpenFileViewer :files="files" :plugins="plugins">
<template #toolbar="ctx">
<button :disabled="!ctx.canPrevious" @click="ctx.previous()">Previous</button>
<span>{{ ctx.index + 1 }} / {{ ctx.length }}</span>
<button :disabled="!ctx.canNext" @click="ctx.next()">Next</button>
<button @click="ctx.download()">Download</button>
<button @click="openApprovalDialog(ctx.file)">Approve</button>
</template>
</OpenFileViewer>
<OpenFileViewer files={files} plugins={plugins}>
<svelte:fragment slot="toolbar" let:ctx>
{#if ctx}
<button disabled={!ctx.canPrevious} on:click={() => void ctx.previous()}>Previous</button>
<span>{ctx.index + 1} / {ctx.length}</span>
<button disabled={!ctx.canNext} on:click={() => void ctx.next()}>Next</button>
<button on:click={ctx.download}>Download</button>
<button on:click={() => openApprovalDialog(ctx.file)}>Approve</button>
{/if}
</svelte:fragment>
</OpenFileViewer>
At the style layer, you can still override classes such as .ofv-toolbar, .ofv-toolbar button, and .ofv-toolbar-search. Custom icon buttons also generate .ofv-toolbar-icon and .ofv-toolbar-label, making alignment, spacing, and truncation easier to control.
| Method | Description |
|---|---|
reload(file?) | Reload the current file or a specified file |
next() / previous() | Switch through the multi-file queue |
goTo(index) | Jump to a specified file |
goToPage(page) | Jump to a 1-based page in PDF, DOCX, OFD, XPS, TIFF, and other paginated previews |
getCurrentIndex() | Get the current index |
resize() | Manually trigger size recalculation |
destroy() | Destroy the viewer and clean up resources |
Each format is integrated through a plugin. A plugin only needs to answer two questions: whether the file matches, and how to render into ctx.viewport.
import type { PreviewPlugin } from "@open-file-viewer/core";
export function customPlugin(): PreviewPlugin {
return {
name: "custom",
match(file) {
return file.extension === "custom";
},
async render(ctx) {
const element = document.createElement("div");
element.textContent = ctx.file.name;
ctx.viewport.append(element);
return {
resize(size) {
console.log("container resized", size);
},
destroy() {
element.remove();
}
};
}
};
}
Plugin constraints:
ctx.viewport.resize(size) when the plugin needs to react to container size changes.destroy() to clean up events, object URLs, timers, Canvas/WebGL resources, and other side effects.packages/
core/ # Framework-agnostic preview core and plugins
react/ # React adapter
vue/ # Vue adapter
svelte/ # Svelte adapter
examples/
vanilla/ # Vanilla JavaScript example
react/ # React example
vue/ # Vue example
svelte/ # Svelte example
doc/ # Website and online experience
pnpm install
pnpm check
Common commands:
pnpm dev:doc
pnpm dev:vanilla
pnpm dev:react
pnpm dev:vue
pnpm dev:svelte
pnpm test
pnpm typecheck
pnpm build
pnpm build:examples
pnpm build:doc
pnpm pack:check
pnpm check runs tests, type checks, package builds, example builds, website build, and package export validation in sequence.
The current workspace version is 1.0.0-rc.1, a release candidate for validating the public API and existing preview capabilities.
| Version | Focus | Release criteria |
|---|---|---|
1.0.0-rc.1 | Validate Core, framework adapters, plugin contracts, and the current format support with integrators | Collect compatibility and real-file feedback before the stable release |
1.0.0 | Stable public API and dependable browser integration | Pass pnpm check, verify consumer builds and representative visual regressions, document supported formats and plugin development |
1.x | Compatible format, fidelity, accessibility, and performance improvements | Preserve the 1.0 public API; use minor or patch releases as appropriate |
2.0.0 | Modular, on-demand format packages and a clearer Worker/WASM resource contract | Release as 2.0 only if import paths or plugin APIs require incompatible changes; provide a migration guide |
3.0.0 | A unified page/scene/text model and browser/server conversion contract for consistent search and printing | Release as 3.0 only if the new rendering contract requires incompatible public API changes |
The 2.0 and 3.0 entries are directions, not dates or commitments to a major-version bump. Backward-compatible work belongs in 1.x.
Open File Viewer will continue improving format preview, framework integration, and real business scenarios. Open source is not easy. If it saves you development time, a free GitHub star is a meaningful way to support future iteration.
461 followers · starred Jun 2026
7 followers · starred Jun 2026
12 followers · starred Sep 2026
8 followers · starred Aug 2026
A framework-agnostic embedded file viewer for vanilla JavaScript, React, Vue and Svelte. Put PDF, Office, images, media, archives, email, drawings, 3D, GIS and engineering files inside one stable container.
See the code
English | Simplified Chinese | 日本語 | 한국어 | Español | Português
Open File Viewer is a file preview SDK for modern web applications. It brings PDFs, Office documents, images, audio and video, archives, emails, drawings, 3D files, GIS data, and source code into one controlled container, with support for vanilla JavaScript, React, Vue, and Svelte.
Website | About | GitHub | NPM Core | React | Vue | Svelte
Most business systems eventually need attachment preview: contracts, spreadsheets, drawings, archives, emails, images, videos, and source files. Open File Viewer is not a PDF-only demo. It is a file preview foundation that can evolve with real product needs over time.
px, %, vh, vw, rem, and calc(), and responds automatically to container changes.pnpm add @open-file-viewer/core
React:
pnpm add @open-file-viewer/core @open-file-viewer/react
Vue:
pnpm add @open-file-viewer/core @open-file-viewer/vue
Svelte:
pnpm add @open-file-viewer/core @open-file-viewer/svelte
PDF preview requires pdfjs-dist when you use pdfPlugin():
pnpm add pdfjs-dist
Angular CLI 20 uses Vite to pre-bundle dependencies. If an application only needs image and PDF preview, use the lightweight entry so the optimizer never enters the Office conversion dependency graph:
import {
createViewer,
fallbackPlugin,
imagePlugin,
pdfPlugin
} from "@open-file-viewer/core/lite";
import "@open-file-viewer/core/style.css";
The lite entry deliberately contains only the viewer runtime, image, PDF, and fallback plugins.
Use the main @open-file-viewer/core entry when the application needs Office, CAD, archive, or
other formats. Do not mix the two entries in the same application.
When using officePlugin() with Vite, exclude the converter from dependency pre-bundling and
externalize its optional Node canvas fallback from the browser build. EMF/WMF conversion in the
browser still uses Canvas:
// vite.config.ts — merge these options into your existing config
import { defineConfig } from "vite";
export default defineConfig({
optimizeDeps: { exclude: ["emf-converter"] },
build: { rollupOptions: { external: ["@napi-rs/canvas"] } }
});
You can also use npm or yarn:
npm install @open-file-viewer/core
yarn add @open-file-viewer/core
Import the shared stylesheet once in your application:
import "@open-file-viewer/core/style.css";
import {
createViewer,
imagePlugin,
videoPlugin,
audioPlugin,
textPlugin,
pdfPlugin,
officePlugin,
archivePlugin,
emailPlugin,
drawingPlugin,
xmindPlugin,
cadPlugin,
model3dPlugin,
gisPlugin,
fallbackPlugin
} from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
const plugins = [
imagePlugin(),
videoPlugin(),
audioPlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin(),
archivePlugin(),
emailPlugin(),
drawingPlugin(),
xmindPlugin(),
cadPlugin(),
model3dPlugin(),
gisPlugin(),
fallbackPlugin()
];
const viewer = createViewer({
container: "#viewer",
file: fileOrUrl,
fileName: "contract.pdf",
width: "100%",
height: "70vh",
fit: "contain",
toolbar: true,
theme: "auto",
plugins
});
viewer.resize();
viewer.destroy();
Use the same plugin list to check support before mounting a viewer:
import { isPreviewSupported } from "@open-file-viewer/core";
const supported = await isPreviewSupported(fileOrUrl, plugins, {
fileName: "contract.pdf",
mimeType: "application/pdf"
});
The check normalizes the source exactly like createViewer() and evaluates
plugin.match() in order. It does not mount DOM or call plugin.render(), and
fallbackPlugin() is not counted as native preview support.
Keep the plugin order identical to the viewer configuration; a matching
fallbackPlugin() is terminal and must not be placed before native plugins.
When PDF.js cannot parse or load a remote PDF URL, the PDF plugin falls back to
the browser's embedded PDF viewer. Different Chromium-based viewers apply
different script requirements inside sandboxed iframes, so
webFallbackScripts: "auto" is the default:
allow-scripts while remaining sandboxed;The host can make the policy stricter, or explicitly trust a same-origin PDF endpoint:
pdfPlugin({ webFallbackScripts: "never" }); // strict sandbox
pdfPlugin({ webFallbackScripts: "always" }); // trusted PDF endpoints only
This setting only affects the remote iframe fallback. It does not change the
normal PDF.js rendering path or force a referrerpolicy value.
PDF.js 5+ uses files from pdfjs-dist/wasm/ to decode formats such as JPEG2000
(JPX) and JBIG2. Serve that directory from your application and pass its public
URL, including the trailing slash, to the plugin:
pdfPlugin({
workerSrc: pdfWorkerSrc,
wasmUrl: "/pdfjs/wasm/"
});
Keep the hosted WASM files on the same version as the installed pdfjs-dist.
PDF.js 4 ignores this forward-compatible option.
Micro-frontend sandboxes (qiankun, micro-app, ...) tear down window message
listeners when a sub-app unmounts, which breaks the setImmediate polyfill that
jszip depends on. JSZip.loadAsync then never resolves, so every zip-based
preview (docx / xlsx / pptx / epub / ofd) hangs on the loading state forever
(see qiankun#2589).
createViewer() detects sandbox flags such as __POWERED_BY_QIANKUN__ and
__MICRO_APP_ENVIRONMENT__ and automatically installs a MessageChannel-based
scheduler that sandboxes cannot break — no configuration needed. On versions up
to 0.1.27, patch it manually in the sub-app entry before any imports:
if (window.__POWERED_BY_QIANKUN__) {
window.setImmediate = (fn, ...args) => setTimeout(fn, 0, ...args);
}
import { FileViewer } from "@open-file-viewer/react";
import { imagePlugin, pdfPlugin, officePlugin, textPlugin } from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
const plugins = [
imagePlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin()
];
export function AttachmentPreview({ file }: { file: File }) {
return (
<FileViewer
file={file}
fileName={file.name}
width="100%"
height="640px"
fit="contain"
toolbar
theme="auto"
plugins={plugins}
/>
);
}
<script setup lang="ts">
import { OpenFileViewer } from "@open-file-viewer/vue";
import { imagePlugin, pdfPlugin, officePlugin, textPlugin } from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
defineProps<{ file: File }>();
const plugins = [
imagePlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin()
];
</script>
<template>
<OpenFileViewer
:file="file"
:file-name="file.name"
width="100%"
height="640px"
fit="contain"
toolbar
theme="auto"
:plugins="plugins"
/>
</template>
<script lang="ts">
import { OpenFileViewer } from "@open-file-viewer/svelte";
import { imagePlugin, pdfPlugin, officePlugin, textPlugin } from "@open-file-viewer/core";
import "@open-file-viewer/core/style.css";
import pdfWorkerSrc from "pdfjs-dist/build/pdf.worker.mjs?url";
export let file: File;
const plugins = [
imagePlugin(),
textPlugin(),
pdfPlugin({ workerSrc: pdfWorkerSrc }),
officePlugin()
];
</script>
<OpenFileViewer
{file}
fileName={file.name}
width="100%"
height="640px"
fit="contain"
toolbar
theme="auto"
{plugins}
/>
| Scenario | What Open File Viewer Provides |
|---|---|
| OA / ERP / CRM attachment centers | A unified container preview for contracts, spreadsheets, images, emails, and archives |
| Cloud drives / knowledge bases / document systems | Multi-file queues, download, search, fullscreen, and theme adaptation |
| Low-code / form systems | Vanilla JS integration without forcing React, Vue, or Svelte |
| Engineering / manufacturing / GIS systems | Recognition and progressive enhancement for CAD, 3D, GIS, and drawing files |
| Developer platforms / log platforms | Text, config, Markdown, code highlighting, and large-file protection |
| Capability | Status |
|---|---|
| Vanilla JS / React / Vue / Svelte integration | Supported |
| Custom container, width, height, and responsive sizing | Supported |
| Multi-file queue, switching, and current index | Supported |
| Toolbar, download, fullscreen, print, and search | Supported |
Light, dark, and auto themes | Supported |
Local File / Blob / URL / ArrayBuffer sources | Supported |
| Plugin protocol and custom fallback | Supported |
| PDF, images, audio/video, text/code | Supported |
| Office, OFD, EPUB, XPS, email, and archives | Basic to enhanced preview |
| CAD, 3D, GIS, drawing boards, and design assets | Detection, basic preview, and ongoing enhancements |
| Category | Plugin | Representative Formats |
|---|---|---|
| Images | imagePlugin() | jpg, png, gif, webp, avif, svg, bmp, tiff, heic, heif |
| Video | videoPlugin() | mp4, webm, mov, m4v, avi, mkv, flv, wmv, m3u8, m2ts |
| Audio | audioPlugin() | mp3, wav, ogg, aac, m4a, flac, opus, mid, wma |
| Text / code | textPlugin() | txt, lrc, md, json, yaml, xml, csv, js, ts, tsx, vue, html, css, py, go, rs, sql, sh |
| PDF / ebooks | pdfPlugin(), epubPlugin(), xpsPlugin() | pdf, epub, xps, oxps |
| Office | officePlugin() | doc, docx, docm, dot, rtf, odt, xls, xlsx, xlsm, xlsb, csv, ppt, pps, pptx, pptm, odp, wps, et, dps |
| OFD | ofdPlugin() | ofd |
| Archives | archivePlugin() | zip, rar, 7z, tar, gz, tgz, bz2, xz |
emailPlugin() | eml, msg, mbox | |
| Drawing / whiteboard | drawingPlugin() | drawio, dio, excalidraw, tldraw |
| Mind maps | xmindPlugin() | xmind |
| CAD / engineering / chip layout | cadPlugin() | dxf, dwg, dwf, step, stp, iges, igs, ifc, skp, sldprt, gds, gdsii, oas, oasis |
| 3D models | model3dPlugin() | gltf, glb, obj, stl, fbx, dae, ply, 3mf, usd, usdz |
| GIS | gisPlugin() | geojson, topojson, kml, kmz, gpx, shp |
| Asset recognition | assetPlugin() | ttf, woff2, psd, ai, eps, sqlite, wasm, parquet, avro |
Preview quality for complex formats depends on browser capabilities, file structure, and the parser used by each plugin. The current version focuses on making every format enter a controlled preview path inside the container. High-fidelity Office, CAD, design, and proprietary binary formats can continue to integrate dedicated engines or server-side conversion.
Legacy ppt / pps files use a local OLE and PowerPoint Binary File Format preview path. It restores slide geometry, positioned text, master bitmap artwork, JPEG/PNG/TIFF assets, and common compressed EMF/WMF graphics without uploading the file. Unsupported drawing records fall back gracefully; use officePlugin({ convert }) when pixel-identical Office rendering is required.
Markdown code blocks display line numbers and a copy button in the upper-right corner. Copying preserves the code text and excludes line numbers; button labels and feedback use the existing textCopy, textCopied, and textCopyFailed messages.
Plugin order matters because the first matching plugin renders the file. For example, csv and tsv can match both textPlugin() and officePlugin(); place officePlugin() earlier if you want spreadsheet-style table preview.
DWG is AutoCAD's proprietary binary format. cadPlugin() can use a high-fidelity WebGL scene, the lightweight built-in SVG path, or an application-provided renderer.
webglDwg to parse DWG in a Worker and draw layers, blocks, hatches, line styles, and text in an interactive CAD canvas. Once configured, errors are surfaced instead of silently switching to SVG.cadPlugin() automatically tries LibreDWG WASM for DWG model-space linework. If the linework looks unreliable but the file contains an embedded thumbnail, it shows the DWG thumbnail. If LibreDWG is not installed, the WASM path is not configured, or parsing fails, it falls back to DWG/DWF metadata, version hints, structure probes, and conversion guidance.cadPlugin({ binaryRenderer }) to integrate your own frontend engine, CADViewer, MxCAD, or a backend service that converts to PNG/PDF/SVG/DXF. binaryRenderer has the highest priority and fully takes over DWG/DWF preview when it returns an instance.Recommended high-fidelity setup:
npm install @mlightcad/cad-simple-viewer@1.5.10 @mlightcad/data-model@1.12.5 lodash-es@4.17.21
Copy libredwg-parser-worker.js and mtext-renderer-worker.js from the installed
@mlightcad/cad-simple-viewer/dist/ directory into a public static directory,
then configure that directory:
cadPlugin({
webglDwg: {
engineLoader: () => import("@mlightcad/cad-simple-viewer"),
workerBaseUrl: "/vendor/cad-engine"
}
});
The explicit engineLoader keeps the optional engine out of builds that do not
configure WebGL DWG preview and gives strict esbuild setups a host-owned import
they can resolve.
The WebGL package is MIT licensed. Its published DWG parser Worker is based on
LibreDWG, so applications should also review the Worker's license requirements.
CAD font resources may be configured with webglDwg.baseUrl; only self-host
font files that your application is licensed to distribute.
Lightweight LibreDWG SVG setup:
npm install @mlightcad/libredwg-web@0.7.4
scripts/copy-libredwg-assets.mjs to the host application. This example
targets the conventional public/ directory used by Vite and Next.js; change
targetRoot if your framework uses a different static directory.import { cp, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const moduleEntry = fileURLToPath(import.meta.resolve("@mlightcad/libredwg-web"));
const packageRoot = dirname(dirname(moduleEntry));
const targetRoot = fileURLToPath(
new URL("../public/vendor/libredwg-web/", import.meta.url)
);
await mkdir(targetRoot, { recursive: true });
await Promise.all([
cp(join(packageRoot, "dist"), join(targetRoot, "dist"), {
recursive: true,
force: true
}),
cp(join(packageRoot, "wasm"), join(targetRoot, "wasm"), {
recursive: true,
force: true
})
]);
{
"scripts": {
"assets:dwg": "node scripts/copy-libredwg-assets.mjs",
"predev": "npm run assets:dwg",
"prebuild": "npm run assets:dwg"
}
}
If the project already defines predev or prebuild, append the asset command
to the existing script instead of replacing it.
dist/ and wasm/ directory layout and configure
the browser-loadable ESM entry:cadPlugin({
libreDwg: {
wasmBaseUrl: "/vendor/libredwg-web/wasm",
workerModuleUrl: "/vendor/libredwg-web/dist/libredwg-web.js",
workerTimeoutMs: 120_000
}
});
After starting the application, verify that both URLs return 200:
/vendor/libredwg-web/dist/libredwg-web.js
/vendor/libredwg-web/wasm/libredwg-web.wasm
For applications deployed below a URL prefix, prepend the application's public
base path to both configuration URLs. Serve .wasm as application/wasm.
These are self-hosted static assets, not files loaded from an external CDN.
The documentation app runs doc/scripts/copy-libredwg-assets.mjs during
predev and prebuild, copying the pinned npm package's dist/ and wasm/
directories into doc/public/vendor/libredwg-web/. Those generated directories
are ignored by Git because they are reproducible and include a roughly 6 MB
WASM binary. Host applications should add an equivalent copy step to their own
build or deployment pipeline. If the deployment platform cannot run that step,
publish the copied directories as static assets instead.
@mlightcad/libredwg-web is an optional GPL-3.0 dependency; it is intentionally
not bundled into the MIT-licensed core package. Applications that enable it
should review the upstream license requirements for their distribution model.
The viewer transfers a DWG buffer copy to a dedicated module worker and
terminates it when the file changes, the viewer is destroyed, or parsing times
out. The ESM/WASM assets must be same-origin or CORS-enabled, and CSP must allow
module workers plus blob: in worker-src. Without a worker URL, the existing
main-thread SVG/thumbnail fallback remains active.
cadPlugin({
async binaryRenderer({ panel, extension, arrayBuffer, fileName }) {
if (extension !== "dwg") return;
const stage = document.createElement("div");
stage.className = "my-dwg-stage";
panel.append(stage);
// Load your DWG engine, worker, fonts, and assets on demand here.
// Example: await renderDwgWithYourEngine(stage, arrayBuffer, { fileName });
return {
destroy() {
stage.remove();
}
};
}
});
createViewer(options: PreviewOptions): FileViewer;
| Option | Type | Default | Description |
|---|---|---|---|
container | HTMLElement | string | Required | Preview container |
file | File | Blob | string | ArrayBuffer | - | Single-file preview source |
files | (PreviewSource | PreviewItem)[] | - | Multi-file preview queue |
initialIndex | number | 0 | Initial file index |
initialPage | number | 1 | Initial 1-based page for paginated previews |
fileName | string | Auto inferred | File name used for extension detection |
mimeType | string | Auto inferred | MIME type. An explicitly supplied text MIME type takes priority over the file extension, so text/plain can show Markdown source while text/markdown renders it. |
width | number | string | Original container width | Preview container width |
height | number | string | Original container height | Preview container height |
zoom | number | 1 | Initial zoom level, where 1 means 100% |
fit | contain | cover | width | height | actual | scale-down | contain | Content fitting mode. When omitted for a direct PDF preview, the continuous reader uses width; pass contain explicitly to fit both dimensions. |
plugins | PreviewPlugin[] | [] | Plugin list, matched in order |
fallback | inline | download | custom | inline | Fallback strategy for unsupported formats |
renderFallback | (ctx) => PreviewInstance | - | Custom fallback renderer |
toolbar | boolean | PreviewToolbarOptions | false | Toolbar configuration |
theme | light | dark | auto | light | Viewer theme |
className | string | - | Extra container class name |
onLoad | (file) => void | - | Callback after loading completes |
onError | (error, file) => void | - | Error callback |
onUnsupported | (file) => void | - | Unsupported-format callback |
toolbar: true enables the default toolbar, including multi-file navigation, zoom, rotate, download, fullscreen, print, and search when supported by the active plugin. You can extend it for business workflows without rewriting the whole viewer.
Set download or print to false when those actions are provided outside the viewer or gated by application permissions. When passing a toolbar options object, explicitly enable the other built-in controls you want to retain:
createViewer({
container: "#viewer",
file,
toolbar: {
zoom: true,
rotate: true,
download: permissions.canDownload,
fullscreen: true,
print: permissions.canPrint,
search: true
},
plugins
});
Use download: false and print: false to remove both buttons completely. The viewer only controls their visibility; permission checks for controls rendered elsewhere remain the application's responsibility.
createViewer({
container: "#viewer",
file,
toolbar: {
zoom: true,
rotate: true,
download: true,
fullscreen: true,
search: true,
labels: {
download: "Download",
fullscreen: "Fullscreen",
search: "Search",
"zoom-in": "Zoom in",
"zoom-out": "Zoom out",
"zoom-reset": "Actual size",
"rotate-right": "Rotate"
},
titles: {
download: "Download current file"
},
icons: {
download: '<svg viewBox="0 0 24 24"><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"/></svg>'
},
order: ["search", "zoom-out", "zoom-in", "zoom-reset", "rotate-right", "download", "fullscreen"]
},
plugins
});
createViewer({
container: "#viewer",
file,
toolbar: {
order: ["download", "favorite", "approve", "share", "fullscreen"],
actions: [
{
id: "favorite",
label: "Favorite",
onClick(ctx) {
favoriteFile(ctx.file);
}
},
{
id: "approve",
label: "Approve",
onClick(ctx) {
openApprovalDialog(ctx.file);
}
},
{
id: "share",
label: "Share",
disabled(ctx) {
return !ctx.file;
},
onClick(ctx) {
shareFile(ctx.file);
}
}
]
},
plugins
});
createViewer({
container: "#viewer",
files,
toolbar: {
render(ctx) {
const bar = document.createElement("div");
bar.className = "business-toolbar";
const name = document.createElement("strong");
name.textContent = ctx.file?.name || "";
const next = document.createElement("button");
next.type = "button";
next.textContent = "Next";
next.disabled = !ctx.canNext;
next.onclick = () => void ctx.next();
const download = document.createElement("button");
download.type = "button";
download.textContent = "Download";
download.onclick = ctx.download;
bar.append(name, next, download);
return bar;
}
},
plugins
});
The render(ctx) context includes file, index, length, previous(), next(), goToPage(page), command(), download(), fullscreen(), print(), search(), and clearSearch(). In core, toolbar.render(ctx) returns a DOM HTMLElement | void; React, Vue, and Svelte expose framework-native toolbar APIs.
<FileViewer
files={files}
plugins={plugins}
renderToolbar={(ctx) => (
<>
<button disabled={!ctx.canPrevious} onClick={() => void ctx.previous()}>Previous</button>
<span>{ctx.index + 1} / {ctx.length}</span>
<button disabled={!ctx.canNext} onClick={() => void ctx.next()}>Next</button>
<button onClick={ctx.download}>Download</button>
<button onClick={() => openApprovalDialog(ctx.file)}>Approve</button>
</>
)}
/>
<OpenFileViewer :files="files" :plugins="plugins">
<template #toolbar="ctx">
<button :disabled="!ctx.canPrevious" @click="ctx.previous()">Previous</button>
<span>{{ ctx.index + 1 }} / {{ ctx.length }}</span>
<button :disabled="!ctx.canNext" @click="ctx.next()">Next</button>
<button @click="ctx.download()">Download</button>
<button @click="openApprovalDialog(ctx.file)">Approve</button>
</template>
</OpenFileViewer>
<OpenFileViewer files={files} plugins={plugins}>
<svelte:fragment slot="toolbar" let:ctx>
{#if ctx}
<button disabled={!ctx.canPrevious} on:click={() => void ctx.previous()}>Previous</button>
<span>{ctx.index + 1} / {ctx.length}</span>
<button disabled={!ctx.canNext} on:click={() => void ctx.next()}>Next</button>
<button on:click={ctx.download}>Download</button>
<button on:click={() => openApprovalDialog(ctx.file)}>Approve</button>
{/if}
</svelte:fragment>
</OpenFileViewer>
At the style layer, you can still override classes such as .ofv-toolbar, .ofv-toolbar button, and .ofv-toolbar-search. Custom icon buttons also generate .ofv-toolbar-icon and .ofv-toolbar-label, making alignment, spacing, and truncation easier to control.
| Method | Description |
|---|---|
reload(file?) | Reload the current file or a specified file |
next() / previous() | Switch through the multi-file queue |
goTo(index) | Jump to a specified file |
goToPage(page) | Jump to a 1-based page in PDF, DOCX, OFD, XPS, TIFF, and other paginated previews |
getCurrentIndex() | Get the current index |
resize() | Manually trigger size recalculation |
destroy() | Destroy the viewer and clean up resources |
Each format is integrated through a plugin. A plugin only needs to answer two questions: whether the file matches, and how to render into ctx.viewport.
import type { PreviewPlugin } from "@open-file-viewer/core";
export function customPlugin(): PreviewPlugin {
return {
name: "custom",
match(file) {
return file.extension === "custom";
},
async render(ctx) {
const element = document.createElement("div");
element.textContent = ctx.file.name;
ctx.viewport.append(element);
return {
resize(size) {
console.log("container resized", size);
},
destroy() {
element.remove();
}
};
}
};
}
Plugin constraints:
ctx.viewport.resize(size) when the plugin needs to react to container size changes.destroy() to clean up events, object URLs, timers, Canvas/WebGL resources, and other side effects.packages/
core/ # Framework-agnostic preview core and plugins
react/ # React adapter
vue/ # Vue adapter
svelte/ # Svelte adapter
examples/
vanilla/ # Vanilla JavaScript example
react/ # React example
vue/ # Vue example
svelte/ # Svelte example
doc/ # Website and online experience
pnpm install
pnpm check
Common commands:
pnpm dev:doc
pnpm dev:vanilla
pnpm dev:react
pnpm dev:vue
pnpm dev:svelte
pnpm test
pnpm typecheck
pnpm build
pnpm build:examples
pnpm build:doc
pnpm pack:check
pnpm check runs tests, type checks, package builds, example builds, website build, and package export validation in sequence.
The current workspace version is 1.0.0-rc.1, a release candidate for validating the public API and existing preview capabilities.
| Version | Focus | Release criteria |
|---|---|---|
1.0.0-rc.1 | Validate Core, framework adapters, plugin contracts, and the current format support with integrators | Collect compatibility and real-file feedback before the stable release |
1.0.0 | Stable public API and dependable browser integration | Pass pnpm check, verify consumer builds and representative visual regressions, document supported formats and plugin development |
1.x | Compatible format, fidelity, accessibility, and performance improvements | Preserve the 1.0 public API; use minor or patch releases as appropriate |
2.0.0 | Modular, on-demand format packages and a clearer Worker/WASM resource contract | Release as 2.0 only if import paths or plugin APIs require incompatible changes; provide a migration guide |
3.0.0 | A unified page/scene/text model and browser/server conversion contract for consistent search and printing | Release as 3.0 only if the new rendering contract requires incompatible public API changes |
The 2.0 and 3.0 entries are directions, not dates or commitments to a major-version bump. Backward-compatible work belongs in 1.x.
Open File Viewer will continue improving format preview, framework integration, and real business scenarios. Open source is not easy. If it saves you development time, a free GitHub star is a meaningful way to support future iteration.
461 followers · starred Jun 2026
7 followers · starred Jun 2026
12 followers · starred Sep 2026
8 followers · starred Aug 2026