Story · Demo
The full workbook viewer
Unlike slides and pages, a spreadsheet is one continuous grid — so XlsxViewer owns its own canvas, sheet-tab bar and zoom slider. Switch sheets along the bottom, click-drag to select a range, press Ctrl/Cmd+C to copy it as TSV, zoom with the slider or Ctrl/Cmd+wheel · trackpad pinch (10–400%), and drag a column/row header border to resize it. Resizing and zooming are view-only — they change what you see, never the loaded file.
import { XlsxViewer } from '@silurus/ooxml/xlsx';
// XlsxViewer owns its canvas, sheet-tab bar and zoom slider — hand it a
// container element (not a canvas). Click-drag selects a range; Ctrl/Cmd+C
// copies it as TSV.
const container = document.getElementById('sheet') as HTMLElement;
const viewer = new XlsxViewer(container, { showZoomSlider: true });
await viewer.load('/sample.xlsx'); In your framework
Mount it in a container
The viewer is a plain TypeScript class — no framework runtime, no peer deps. Create it on mount,
call load(), and let it go on unmount.
import { useEffect, useRef } from 'react';
import { XlsxViewer } from '@silurus/ooxml/xlsx';
export function Viewer({ src }: { src: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const container = ref.current;
if (!container) return;
const viewer = new XlsxViewer(container, { showZoomSlider: true });
void viewer.load(src);
return () => viewer.destroy();
}, [src]);
return <div ref={ref} />;
} <script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from 'vue';
import { XlsxViewer } from '@silurus/ooxml/xlsx';
const props = defineProps<{ src: string }>();
const container = ref<HTMLDivElement>();
let viewer: XlsxViewer | undefined;
onMounted(() => {
viewer = new XlsxViewer(container.value as HTMLDivElement, { showZoomSlider: true });
void viewer.load(props.src);
});
onBeforeUnmount(() => viewer?.destroy());
</script>
<template>
<div ref="container" />
</template> <script lang="ts">
import { onMount } from 'svelte';
import { XlsxViewer } from '@silurus/ooxml/xlsx';
export let src: string;
let container: HTMLDivElement;
onMount(() => {
const viewer = new XlsxViewer(container, { showZoomSlider: true });
void viewer.load(src);
return () => viewer.destroy();
});
</script>
<div bind:this={container}></div> import { XlsxViewer } from '@silurus/ooxml/xlsx';
const container = document.getElementById('viewer') as HTMLDivElement;
const viewer = new XlsxViewer(container, { showZoomSlider: true });
await viewer.load('/sample.xlsx'); API reference
Options & methods
Every public option and method, straight from the source. Types omitted for brevity are exported from the package — your editor will autocomplete the rest.
XlsxViewer
Full workbook viewer. Takes a container <div> (not a canvas) — it manages its own canvas, sheet-tab bar and zoom slider. Drag-to-resize columns/rows and zoom are view-only: they change the on-screen view only and never modify the loaded file.
new XlsxViewer(container: HTMLElement, options?: XlsxViewerOptions) Options
| Option | Type | Default | Description |
|---|---|---|---|
cellScale | number | 1 | Scale factor for cell/header dimensions (0.5 = half size). |
showZoomSlider | boolean | true | Show the Excel-style zoom slider at the end of the tab bar. Zooming (slider, Ctrl/⌘+wheel, trackpad pinch) is view-only. |
zoomMin / zoomMax | number | 0.1 / 4 | Zoom slider bounds as scale factors (10%–400%). |
resizable | boolean | true | Allow resizing columns/rows by dragging header borders. View-only — it changes the on-screen view only and never modifies the loaded file. Set false to disable. |
selectionColor | string | '#1a73e8' | Accent color for the cell-selection rectangle (any CSS color). The fill is the same color at 8% opacity. |
hiddenSheetMode | 'show' | 'skip' | 'dim' | 'show' | How hidden / very-hidden sheets (`<sheet state>`, §18.2.19) appear in the tab bar. `show` renders a tab like any other; `skip` hides the tab (`display:none`) and makes sequential navigation jump over it; `dim` renders the tab at reduced opacity. Mirrors pptx `hiddenSlideMode`. |
useGoogleFonts | boolean | false | Load metric-compatible webfonts and non-Latin script fallbacks (Noto Arabic / CJK KR·SC·TC·JP / Cyrillic / Hebrew / Thai / Devanagari) from Google Fonts so layout matches Office and non-Latin text never falls back to tofu. Off by default for privacy. |
maxZipEntryBytes | number | 512 MiB | Per-entry ZIP decompression cap (zip-bomb guard). Lower it for untrusted input. Zero / negative values fall back to the default. |
math | MathRenderer | undefined | Opt-in OMML equation engine (MathJax + STIX Two Math, ~3 MB). Import it from the separate @silurus/ooxml/math entry — `import { math } from "@silurus/ooxml/math"` — and pass it to render equations. Omit it and equations are skipped, and the engine is left out of your build. When passed, the engine ships as a standalone asset fetched lazily the first time a document contains an equation. |
mode | 'main' | 'worker' | 'main' | 'main' renders on the main thread (default). 'worker' renders the whole viewer off the main thread — every frame is produced in a Web Worker and painted via a `bitmaprenderer` context — so document rendering never blocks the UI. Scroll, sheet tabs, zoom and (xlsx) cell selection are unchanged. Requires Worker + OffscreenCanvas. The pptx/docx text-selection overlay and in-document find work in 'worker' mode too (per-run geometry crosses the worker boundary); equations still require 'main'. Trade-off: each frame crosses the worker boundary as an ImageBitmap, so an individual render can be marginally slower than 'main' — the win is a responsive main thread, not raw render speed. |
onScaleChange | (scale: number) => void | — | Called when the zoom factor changes (setScale / fitWidth / fitPage / zoomIn / zoomOut), with the clamped factor (1 = 100%). |
onHyperlinkClick | (target: HyperlinkTarget) => void | — | Called when a hyperlink is clicked. `target` is `{ kind: 'external', url }` or `{ kind: 'internal', ref, slideIndex? }`. When supplied, the callback fully owns the click (the default external-open / internal-navigation is not run). External URLs are scheme-sanitized (http / https / mailto / tel only); internal targets resolve to a docx bookmark / pptx slide jump / xlsx defined name or cell. |
enableHyperlinks | boolean | true | Master switch for hyperlink interactivity. Set `false` to disable it entirely: no hit-testing, no pointer cursor over links, no default navigation, and `onHyperlinkClick` is never called. Links still render exactly as authored but are inert, like plain text. |
onReady | (sheetNames: string[]) => void | — | Called once the workbook is parsed. |
onSheetChange | (index: number, total: number) => void | — | Called when the active sheet changes; `total` is the sheet count. Read the name via `sheetNames[index]`. |
onSelectionChange | (sel: CellRange | null) => void | — | Called when the selected range changes; null clears it. |
onError | (err: Error) => void | — | Called on parse or render errors. |
Methods
load(source: string | ArrayBuffer): Promise<void> | Load a workbook from a URL or ArrayBuffer and render the first sheet. |
goToSheet(index: number): Promise<void> | Show a specific sheet (0-indexed, clamped). |
nextSheet(): Promise<void> | Advance one sheet. |
prevSheet(): Promise<void> | Go back one sheet. |
get sheetIndex(): number | Current sheet index. |
get sheetCount(): number | Total sheets (0 until loaded). |
get sheetNames(): string[] | Names of all sheets. |
get selection(): CellRange | null | The current selected range. |
getScale(): number | The current zoom factor (1 = 100%). |
setScale(scale: number): void | Set the zoom factor (1 = 100%), clamped to [zoomMin, zoomMax] and snapped to whole percent; re-renders and fires onScaleChange when it changes. View-only. |
fitWidth(): void | Fit the used data range WIDTH (row header + used columns) to the canvas area (routes through setScale). Defers when unloaded / unlaid-out. |
fitPage(): void | Fit the used data range WIDTH and HEIGHT inside the canvas area so the whole used range is visible without scrolling — takes the tighter of the two fits. Defers when unloaded / unlaid-out. |
findText(query: string, opts?: { caseSensitive?: boolean }): Promise<FindMatch<XlsxMatchLocation>[]> | Full-text search across the whole document; highlights every hit and returns them in document order. Each match carries `matchIndex`, the matched `text`, and its `location`. Case-insensitive by default. |
findNext(): Promise<FindMatch<XlsxMatchLocation> | null> | Move to the next match (wrap-around), navigate to it if needed, and draw it in the active-match colour. Returns the now-active match, or null when there are none. Call findText first. |
findPrev(): Promise<FindMatch<XlsxMatchLocation> | null> | Move to the previous match (wrap-around from first to last). |
clearFind(): void | Clear all highlights and reset the find state. |
setSelectionColor(color: string): void | Change the selection accent color at runtime (any CSS color). |
get hiddenSheetMode(): "show" | "skip" | "dim" | The current hidden-sheet mode. |
setHiddenSheetMode(mode: "show" | "skip" | "dim"): Promise<void> | Switch the hidden-sheet mode at runtime: restyle the tabs and re-render. Entering `skip` while on a hidden sheet advances to the nearest visible sheet. |
getCellAt(clientX: number, clientY: number): CellAddress | null | Hit-test a viewport coordinate to a cell address. |
get canvasElement(): HTMLCanvasElement | The underlying canvas the grid is drawn on. |
destroy(): void | Tear down the worker and release resources. |
XlsxWorkbook
Headless engine — parse once, render any sheet viewport into any canvas you supply.
await XlsxWorkbook.load(source, options?) Options
| Option | Type | Default | Description |
|---|---|---|---|
useGoogleFonts | boolean | false | Load metric-compatible webfonts and non-Latin script fallbacks (Noto Arabic / CJK KR·SC·TC·JP / Cyrillic / Hebrew / Thai / Devanagari) from Google Fonts so layout matches Office and non-Latin text never falls back to tofu. Off by default for privacy. |
wasmUrl | string | URL | bundled asset | Override the URL the parser worker fetches the WebAssembly module from. By default each format resolves the `*_parser_bg.wasm` asset that ships next to its bundle (relative to the module URL); set this to serve it from a CDN or a self-hosted path instead (a relative value resolves against the document URL). Pointing it at a mismatched or missing file makes load() reject when the worker instantiates it. |
maxZipEntryBytes | number | 512 MiB | Per-entry ZIP decompression cap (zip-bomb guard). Lower it for untrusted input. Zero / negative values fall back to the default. |
workerTimeoutMs | number | unlimited | Reject the parse if the worker does not answer within this many ms — an opt-in safety net for a wedged / crashed worker that would otherwise leave load() pending forever. Unlimited by default (a large document with heavy media can legitimately take tens of seconds). A worker that throws or fails to load already rejects immediately regardless; this only covers the "silent, never-responds" case. |
math | MathRenderer | undefined | Opt-in OMML equation engine (MathJax + STIX Two Math, ~3 MB). Import it from the separate @silurus/ooxml/math entry — `import { math } from "@silurus/ooxml/math"` — and pass it to render equations. Omit it and equations are skipped, and the engine is left out of your build. When passed, the engine ships as a standalone asset fetched lazily the first time a document contains an equation. |
mode | 'main' | 'worker' | 'main' | 'main' parses in a worker and renders on the main thread (default). 'worker' parses AND renders entirely inside the worker; the main thread only paints the ImageBitmap returned by the render*ToBitmap method via a `bitmaprenderer` context. Requires Worker + OffscreenCanvas. The canvas-target render methods are unavailable in 'worker' mode, and equations require 'main'. Trade-off: each frame is transferred from the worker as an ImageBitmap, so a single render can be marginally slower than 'main' — the win is that the main thread never blocks. |
Methods
static load(source, options?): Promise<XlsxWorkbook> | Parse a workbook from a URL or ArrayBuffer. |
get sheetNames(): string[] | Names of all sheets. |
get sheetCount(): number | Total sheets. |
renderViewport(canvas, sheetIndex, viewport, opts?: { width?, height?, dpr?, cellScale?, onTextRun? }): Promise<void> | Render a row/col window of a sheet into the given canvas. `onTextRun` receives each text cell as `XlsxTextRunInfo` with required `sheetName` and A1 `cellRef` identity. Equations in shapes render when a `math` engine was passed to `load`. Unavailable in `mode: "worker"` — use renderViewportToBitmap. |
renderViewportToBitmap(sheetIndex, viewport, opts: { width, height, dpr?, cellScale? }): Promise<ImageBitmap> | Render a sheet viewport and return it as an ImageBitmap (both modes; in worker mode the render runs off the main thread). `width` and `height` are required — a worker has no DOM element to measure. Equations in shapes are skipped in `mode: "worker"` (they require `mode: "main"`). The bitmap is caller-owned: pass it to `transferFromImageBitmap` (which consumes it) or call `bitmap.close()`. |
resolveValidationList(sheetIndex, formula1): Promise<ResolvedList> | Resolve a list-type data-validation `formula1` (ECMA-376 §18.3.1.32) into the allowed values to display — inline quoted list, a range reference (each cell’s display string), or `{ kind: 'formula' }` for named ranges. Read-only. |
destroy(): void | Release the worker. |