- Overview
- Anatomy
- Header
- Navigation
- Renderers
The XLSX Viewer renders an Excel workbook the way the PDF Viewer
renders a document: a familiar controls row above the content. It parses the
.xlsx (an Office Open XML zip) entirely in the browser with
SheetJS — via the patched, zero-dependency
@e965/xlsx republish — so there is
no server and no upload.
Each sheet renders as a virtualized grid: only the rows and columns inside the scroll window are ever in the DOM, so a workbook with tens of thousands of rows stays smooth. Cells show the workbook's formatted values (numbers, dates, currencies as Excel displays them), numbers are right-aligned, and the column letters / row numbers match real spreadsheet cell references.
It is built without useEffect for data loading: the workbook loads with React's
use() and Suspense. The parse runs in a Web Worker, which flattens each
sheet into a sparse compact form (non-empty cell indexes, one text blob, and
typed arrays) and hands that back — so even a 60k-row workbook never freezes the
UI thread. Cells are then read synchronously off that compact form.
"use client";
import * as React from "react";
import { XlsxViewer } from "@/components/ui/xlsx-viewer";
export function XlsxViewerDemo() {
return (
<div className="h-[600px] min-h-0">
<XlsxViewerInstallation
pnpm dlx shadcn@latest add @retab/xlsx-viewer
This pulls in @e965/xlsx (a patched, dependency-free SheetJS build on npm).
Usage
import { XlsxViewer } from "@/components/ui/xlsx-viewer";
export function Example() {
return (
<XlsxViewer
source={{ kind: "url", url: "/workbook.xlsx", fileName: "workbook.xlsx" }}
className="h-[600px]"
/>
);
}Switch sheets with the tab strip along the bottom; zoom scales the whole grid (row height, column width, and font together).
How it performs
- Off-thread parse. The workbook is parsed in a Web Worker — no conversion service, and no UI freeze. The worker flattens each sheet to sorted non-empty cell indexes, a text blob, and transferable typed arrays, so crossing the worker boundary avoids cloning every cell object.
- Row + column virtualization. Built on Retab's local fixed-grid virtualizer: only the visible window of cells is rendered, with tight overscan and pooled rows to avoid blank flashes while scrolling.
- Synchronous cell reads. Each visible cell is resolved against the sparse
index and then read as a string slice off the compact sheet — no per-cell
objects and no dense
rows * columnstext matrix.
Fidelity
Values render as the workbook formats them (so 1234.5 may show as 1,234.50
and date serials as dates). Cell styling — fills, fonts, borders, conditional
formatting — and charts are not rendered; this is a data viewer, not a
pixel-exact Excel clone. Merged cells show their value in the top-left cell.
API Reference
| Prop | Type | Description |
|---|---|---|
source | UrlViewerSource | BlobViewerSource | Canonical spreadsheet source. URL sources load through fetch; Blob sources load directly from the Blob. |
controls | boolean | Show the zoom/download controls. Defaults to true. |
defaultSheetIndex | number | Sheet shown first. Defaults to 0. |
onSheetChange | (index: number) => void | Fires with the active sheet index on tab switch and imperative sheet changes. |
activeCell | { sheet: number; row: number; col: number } | null | A cell to highlight (0-based row + column on sheet). |
header | ReactNode | Full-width strip below the controls (e.g. a legend). |
aside | ReactNode | Left rail alongside the grid. |
bare | boolean | Drop the outer border/background so the viewer fills its container. |
isolateStyles | boolean | Render the scrolling grid inside a shadow root so the host page's CSS — especially :has() selectors, whose invalidation Blink doesn't scope by contain — can't reach it. On :has()-heavy pages this collapses per-scroll style recalc and keeps scrolling at refresh rate. The page's author CSS is copied in so utilities resolve and theme variables inherit through the boundary. Defaults to false. When on, host CSS can't style the grid's internals. |
className | string | Optional class on the viewer root element. |
Imperative handle
Attach a ref when source-link UI needs to drive the viewer:
const ref = React.useRef<XlsxViewerHandle>(null);
ref.current?.scrollToCell(0, 4, 2, { behavior: "auto" });| Method | Description |
|---|---|
scrollToCell | Scrolls to public compatibility coordinates (sheet, row, col), all 0-based. Calls made before workbook metadata loads are replayed after load if the target is in bounds. |
getViewportElement() | Returns the scrolling grid viewport after load, or null before the viewport is mounted. |
Source
"use client";
import * as React from "react";
import {
createViewerResource,
type ViewerResource,
} from "@/lib/viewer-resource";
import { useIsClient } from "@/components/ui/use-is-client";
import { ViewerErrorBoundary } from "@/components/ui/viewer-error";
import { XlsxViewerFallback } from "./xlsx-viewer-chrome";