@swisper.ai/web-file-preview
v0.0.14
Published
In-browser preview for PDF, Word, Excel, PowerPoint, CSV, image, audio and video files
Readme
@swisper.ai/web-file-preview
In-browser preview for PDF, Word, Excel, PowerPoint, CSV, image, audio and video files. It is a compound React component with no loader or error UI of its own: you compose those, and the file must be reachable from the browser (same origin or CORS) or passed in as a Blob. It also shows Markdown, HTML (only ever in an <iframe sandbox="">) and plain text, and FileThumbnail is its small counterpart for lists of files, drawing each file's first page, sheet or slide.
Boundary
Code belongs here if it turns file bytes into something on screen: a renderer, the viewer shell around it (toolbar, thumbnails, zoom), a thumbnail of the file, or the engine setup they need. Chat-specific UI, such as the attachment card that opens a preview, belongs in @swisper.ai/web-chat-ui. Heavy engines stay here, so chat consumers don't download PDFium or the spreadsheet engine.
Install
npm install @swisper.ai/web-file-preview| Peer | Range | Why |
| ----------- | ---------- | -------------------- |
| react | >=18.0.0 | Components and hooks |
| react-dom | >=18.0.0 | Rendering |
The PDF (@embedpdf/*), spreadsheet, presentation and Word-thumbnail (@extend-ai/*), Word (docx-preview) and CSV (papaparse) engines are bundled into the package, and each loads lazily the first time a file of that kind is shown; the Markdown renderer (react-markdown) is a regular dependency, also loaded lazily. The package depends on @swisper.ai/ui, whose peers (@mui/material, @mui/system, @mui/icons-material, @emotion/react, @emotion/styled) apply too.
Setup
- Load the styles (see Styles).
- Optionally, mount
FilePreviewThemeProvideronce, next to wherever your app decides its color scheme. Without it, previews are dark. - Optionally, with a strict CSP or no internet access, self-host the engines' WASM (see Self-hosting the WASM).
- Check the file name, then render a preview inside a parent with a height:
import {
FilePreview,
FilePreviewError,
FilePreviewFile,
FilePreviewLoader,
isFilePreviewSupported,
} from '@swisper.ai/web-file-preview';
export function Attachment({ url, name }: { url: string; name: string }) {
if (!isFilePreviewSupported(name)) {
return <a href={url}>Download {name}</a>;
}
return (
<FilePreview src={url} fileName={name} className="h-[600px]">
<FilePreviewFile />
<FilePreviewLoader>Loading…</FilePreviewLoader>
<FilePreviewError>{() => 'This file could not be opened.'}</FilePreviewError>
</FilePreview>
);
}FileThumbnail composes the same way, with FileThumbnailLoader, FileThumbnailPreview and FileThumbnailFallback as children.
Self-hosting the WASM
Call these once at startup, before the first preview:
| Engine | Default | Self-host |
| -------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| PDF (PDFium) | Loads from cdn.jsdelivr.net, pinned to the engine's version | Copy @embedpdf/pdfium/dist/pdfium.wasm to a URL you serve, and call setFilePreviewPdfWasmSource(url) |
| Spreadsheets | Inlined in the package; no configuration needed | Copy @extend-ai/react-xlsx/duke_sheets_wasm_bg.wasm, and call setFilePreviewSpreadsheetWasmSource(url) |
| Presentations | Inlined in the package; no configuration needed | Copy @extend-ai/react-pptx/pptx_wasm_bg.wasm, and call setFilePreviewPresentationWasmSource(url). Pass a URL: the parser runs in a worker, which can't receive a Response |
| Word thumbnails (FileThumbnail only) | Inlined in the package; no configuration needed | Copy @extend-ai/react-docx/docx_wasm_bg.wasm, and call setFilePreviewDocxWasmSource(url) |
Compiling the package from source
The built package needs no bundler configuration. If you alias it to its src/ instead (in a monorepo, say), Vite's dev pre-bundling moves the spreadsheet and presentation engines away from the worker and WASM files they load relative to themselves. Exclude them, include their CommonJS dependencies back, and build workers as ES modules:
export default defineConfig({
optimizeDeps: {
exclude: ['@extend-ai/react-xlsx', '@extend-ai/react-pptx'],
include: [
'@extend-ai/react-xlsx > regl',
'@extend-ai/react-pptx > regl',
'@extend-ai/react-pptx > react-dom/server',
'@extend-ai/react-pptx > topojson-client',
],
},
worker: { format: 'es' },
});Styles
Pick one of the two options.
| Your app | Use |
| ----------------------------------- | ------------------------------------------------------------------------------------ |
| Builds its own CSS with Tailwind v4 | @import '@swisper.ai/web-file-preview/tailwind.css'; after the design-system theme |
| Has no Tailwind | import '@swisper.ai/web-file-preview/index.css'; once, at the app's entry |
With Tailwind, install @swisper.ai/design-system too and import its theme first:
@import '@swisper.ai/design-system/src/tailwind.css'; /* tailwindcss + theme: colors, radii, fonts */
@import '@swisper.ai/web-file-preview/tailwind.css';The prebuilt index.css instead brings its own preflight, theme and every utility the @swisper.ai packages use, so loading it next to your own Tailwind build ships everything twice, and whichever file loads last wins wherever the two conflict.
tailwind.css holds only source directives:
@source "./dist";
@source not "./dist/engines";dist/engines/ holds the preview engines' payload: inlined WASM, inline workers and map data. These files contain no class names and have lines several MB long, so scanning them takes minutes. If you list sources yourself, exclude that directory. Don't exclude chunk files, because their hashed names change every release:
@source "../../../node_modules/@swisper.ai/*/dist/**/*.{js,mjs,css}";
@source not "../../../node_modules/@swisper.ai/web-file-preview/dist/engines";API
Props, accessibility notes and live examples are on each component's page in the Echo DS Storybook.
| Export | Why it exists |
| -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| FilePreview | Root. Takes exactly one of src or fileBlob, plus fileName, and owns the load status. src is fetched up front, so a 404 or CORS error becomes a real error instead of a blank renderer. |
| FilePreviewFile | Picks the renderer from the fileName extension. |
| FilePreviewLoader, FilePreviewError | Your loading and failure UI, shown as overlays in their status. Without children the loader shows a skeleton; the error takes ({ error }) => ReactNode. |
| FileThumbnail, FileThumbnailPreview, FileThumbnailLoader, FileThumbnailFallback | A tile for lists of files, composed like FilePreview. Draws the file itself (first page, sheet or slide; rendered Markdown; a sandboxed HTML page; a CSV's first rows), or your previewImageUrl; otherwise the file name. It loads nothing until it is on screen. |
| FilePreviewThemeProvider | Sets dark or light for every preview and thumbnail below it. File content is never recolored. |
| isFilePreviewSupported(fileName), isFileThumbnailSupported(fileName) | Lets you offer a download instead of rendering a preview that can only fail. Both check the same list. |
| getFileKind(fileName), getFileExtension(fileName), FileKind | The kind (pdf, image, spreadsheet, …) a file name maps to, for your own icons or labels. |
| FILE_EXTENSIONS_BY_KIND | The supported extensions, for file pickers and accept attributes. |
| setFilePreviewPdfWasmSource, setFilePreviewSpreadsheetWasmSource, setFilePreviewPresentationWasmSource, setFilePreviewDocxWasmSource | Point an engine at a WASM file you host yourself (see Self-hosting the WASM). |
Troubleshooting
| Symptom | Cause | Fix |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| "Invalid hook call", or a context is missing | The app resolves two copies of React | Add resolve: { dedupe: ['react', 'react-dom'] } to the Vite config |
| Components render without layout or colors | Tailwind never scans the package, because it skips node_modules by default | Import the package's tailwind.css in your Tailwind entry, or load its prebuilt index.css |
| Layout is right, but colors or radii are wrong or missing | The design-system theme isn't loaded, so theme utilities such as bg-paper-elevation-1 are empty | Import @swisper.ai/design-system/src/tailwind.css before the package's tailwind.css |
| Your own base styles (fonts, headings, resets) get overridden | The prebuilt index.css is loaded next to your own Tailwind build, and its preflight wins | Use one of the two: drop index.css and import tailwind.css |
| Spacing looks wrong after upgrading (huge gaps, tiny icons) | A number in a spacing class is pixels: gap-8 is 8px, not Tailwind's default 32px | Write the pixel value you want |
| Styles are missing or doubled, or the MUI theme doesn't apply | The app resolves two copies of @emotion/react or @mui/material | Add them to resolve.dedupe in the Vite config, next to react and react-dom |
| ERR_UNSUPPORTED_DIR_IMPORT when a test imports the package | dist/ uses extensionless imports that only a bundler resolves, and Vitest externalizes it | Set server.deps.inline: [/@swisper\.ai\//] in the Vitest config |
| Tailwind build:css or the dev server start takes minutes | Tailwind is scanning dist/engines/ | Use the tailwind.css import, or add @source not ".../@swisper.ai/web-file-preview/dist/engines" |
| A class is missing after a manual @source not on chunk files | Those chunks also hold UI code | Exclude dist/engines only |
| [web-file-preview] error thrown during render | FilePreview got both src and fileBlob, or neither; or a part is rendered outside FilePreview | Pass exactly one of src or fileBlob, and keep FilePreviewFile, FilePreviewLoader and FilePreviewError inside it |
| The preview has zero height | FilePreview fills its parent (size-full), and the parent has no height | Give the parent (or className) an explicit height |
| Every PDF fails with a CSP violation for cdn.jsdelivr.net | PDFium loads from the CDN by default | Self-host pdfium.wasm and call setFilePreviewPdfWasmSource |
| "Worker request failed" for spreadsheets or presentations in the Vite dev server | You compile the package from source, and Vite pre-bundled the engines | Add the config from Compiling the package from source |
| The error slot shows for a file that opens in a new tab | The server lacks Access-Control-Allow-Origin for this origin | Fix CORS on the server, or fetch the file yourself and pass it as fileBlob |
| A PDF, Office, Markdown or CSV thumbnail shows the file name instead of the file | FileThumbnail downloads those kinds, and the server refuses this origin (CORS) | Fix CORS on the server, or pass a server-rendered previewImageUrl |
| An HTML thumbnail or preview stays blank | The server forbids framing (X-Frame-Options, CSP frame-ancestors) | Allow this origin to frame it, or pass the file as fileBlob |
| A .doc or .svg file isn't supported | Legacy .doc has no faithful browser renderer, and SVG can carry script on the app's origin | Convert .doc to PDF or .docx on the server; show SVGs another way |
