← All formats

XLSX

Render .xlsx workbooks to Canvas, your way.

Every demo below is the real library rendering a sample Excel workbook (.xlsx) live in your browser — and the exact code that produces it. The same patterns power the Storybook stories.

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.

sample-1.xlsx live · WASM
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} />;
}

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

OptionTypeDefaultDescription
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

OptionTypeDefaultDescription
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.