The CSV Viewer renders CSV or TSV text as a sortable, virtualized table. It uses TanStack Table for the table model and TanStack Virtual for row and column virtualization, so it stays smooth with tens of thousands of rows. Parsing is handled by a small, dependency-free RFC 4180 parser.
Installation
pnpm dlx shadcn@latest add @retab/csv-viewer
Usage
import { CsvViewer } from "@/components/ui/csv-viewer"
export function Example({ csv }: { csv: string }) {
return <CsvViewer value={csv} height={480} />
}You can also pass already-parsed data:
import { CsvViewer } from "@/components/ui/csv-viewer"
import { parseCsv } from "@/lib/csv"
const data = parseCsv(text, { delimiter: "\t" }) // { columns, rows }
;<CsvViewer data={data} />Features
- Virtualized rows and columns — only the visible window of cells is rendered (
@tanstack/react-virtual), so wide and tall tables stay fast. Toggle with thevirtualizedprop. - Sortable columns — click a header to sort (
@tanstack/react-table). - Sticky header and row numbers, horizontal scroll for wide tables.
- Self-contained, incremental parser — handles quoted fields, escaped quotes, and delimiters/newlines inside quotes, across chunk boundaries.
- Off-thread streaming — pass a
File/Blobassourceto parse in a Web Worker (or a time-sliced main-thread fallback) with rows arriving progressively.
Virtualization
Virtualization is on by default and keeps the table fast with large datasets — only the visible window of rows and columns is rendered. For small tables you can render every cell instead:
<CsvViewer value={csv} virtualized={false} />Streaming large files
value parses synchronously, which is fine for small inputs but blocks the main
thread for large ones. For big files, pass a File/Blob (or a string) as
source instead: it's parsed off the render path — in a Web Worker when
available, falling back to a time-sliced main-thread reader — and rows stream in
progressively. The parser is incremental, so it never needs the whole file in
memory at once during parsing.
// e.g. straight from a file input
<CsvViewer source={file} />
// opt out of the worker (time-sliced main thread instead)
<CsvViewer source={file} worker={false} />Props
| Prop | Type | Description |
|---|---|---|
src | string | URL of a CSV/TSV file (same-origin or CORS-enabled). Fetched, then streamed like source. Takes precedence over value/data/source. |
value | string | Raw CSV/TSV text, parsed synchronously. Provide this, data, or source. |
data | ParsedCsv | Pre-parsed { columns, rows }. |
source | Blob | File | string | A large source parsed off the render path, streamed in progressively. |
worker | boolean | Parse source in a Web Worker when available (falls back to main thread). Defaults to true. |
batchSize | number | Rows per progressive batch when streaming a source. Defaults to 5000. |
delimiter | string | Field delimiter for value/source (defaults to ,; use \t for TSV). |
hasHeader | boolean | Treat the first record as a header. Defaults to true. |
showRowNumbers | boolean | Show the leading row-number column. Defaults to true. |
virtualized | boolean | Virtualize rows and columns with TanStack Virtual. Defaults to true. |
overscan | number | Rows rendered beyond the visible window when virtualizing (and the default for columns). Defaults to 8. |
columnOverscan | number | Columns rendered beyond the visible window. Defaults to overscan. |
rowHeight | number | Row height in pixels. Defaults to 33. |
columnWidth | number | Column width in pixels. Defaults to 180. |
height | number | Scroll viewport height in pixels. Defaults to 480. |
scale | number | Baseline zoom multiplier on row height, column width, and font size. Defaults to 1. The footer controls multiply on top of this. |
showZoom | boolean | Show the footer zoom controls. Defaults to true. Set false when a host drives zoom through scale. |
label | string | Accessible label for the table (aria-label). Defaults to "CSV data". |
isolateStyles | boolean | Render the scrolling table 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 (~33ms → ~0.4ms) 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, the table is client-rendered only and host CSS can't style its internals. |
className | string | Optional class on the viewer container. |
Accessibility
The viewer renders ARIA table semantics (role="table" with rowgroup, row,
columnheader, rowheader, and cell roles), aria-rowcount / aria-colcount
reflecting the full dataset (not just the virtualized window), and aria-sort on
sortable column headers. Each part also exposes a data-slot attribute
(csv-header, csv-header-cell, csv-body, csv-row, csv-row-number,
csv-cell) so you can target it in CSS.