The XLSX Viewer renders an Excel workbook the way the PDF Viewer
renders a document: a familiar toolbar shell around 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 compact form (one text blob plus 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 (a typed-array index + a string slice).
Installation
pnpm dlx shadcn@latest add @retab/xlsx-viewer
This pulls in @e965/xlsx (a patched, dependency-free SheetJS build on npm) and
@tanstack/react-virtual for windowing.
Usage
import { XlsxViewer } from "@/components/ui/xlsx-viewer"
export function Example() {
return <XlsxViewer src="/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 a text blob + transferable typed arrays, so crossing the worker boundary costs a string clone rather than a clone of every cell object.
- Row + column virtualization. Built on
@tanstack/react-virtual: only the visible window of cells is rendered, with a small overscan to avoid blank flashes while scrolling, and rows that stay in view are skipped on re-render. - Synchronous cell reads. Each cell is a typed-array index plus a string slice off the compact sheet — no per-cell objects — so switching sheets is instant and memory stays flat no matter how large the sheet.
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.
Props
| Prop | Type | Description |
|---|---|---|
src | string | URL of the .xlsx/.xls (same-origin or CORS-enabled). |
toolbar | boolean | Show the zoom/download toolbar. Defaults to true. |
downloadFileName | string | Filename for the download button. |
defaultSheetIndex | number | Sheet shown first. Defaults to 0. |
onSheetChange | (index: number) => void | Fires with the active sheet index on tab switch. |
header | ReactNode | Full-width strip below the toolbar (e.g. a legend). |
aside | ReactNode | Left rail alongside the grid. |
bare | boolean | Drop the outer border/background so the viewer fills its container. |
className | string | Optional class on the viewer shell. |