GitHub

CSV

A virtualized CSV/TSV table with sortable columns, built on Retab's fixed-grid virtualizer.

The CSV Viewer renders CSV or TSV text as a sortable, virtualized table. It renders straight from the raw rows — the table model, sorting, and fixed-grid virtualization are hand-rolled, with no table library — so it stays smooth with tens of thousands of rows. Parsing is handled by a small, dependency-free RFC 4180 parser.

0 rows
100%

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
      source={{ kind: "text", text: csv, fileName: "data.csv" }}
      height={480}
    />
  );
}

You can also pass already-parsed data as a table source:

import { parseCsv } from "@/lib/csv";
import { CsvViewer } from "@/components/ui/csv-viewer";
 
const data = parseCsv(text, { delimiter: "\t" }); // { columns, rows }
<CsvViewer
  source={{
    kind: "table",
    table: data,
    fileName: "data.tsv",
    dialect: { delimiter: "\t", hasHeader: true },
  }}
/>;

Features

  • Virtualized rows and columns — only the visible window of cells is rendered by the local fixed-grid virtualizer, so wide and tall tables stay fast.
  • Sortable columns — click a header to sort. Sorting keeps only a lightweight array of row indices, so the raw rows are never copied.
  • 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/Blob as source to parse in a Web Worker (or a time-sliced main-thread fallback) with rows arriving progressively.

Streaming large files

Inline text sources parse synchronously, which is fine for small inputs but blocks the main thread for large ones. For big files, pass a File/Blob as a Blob 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.

0 rows
100%
// e.g. straight from a file input
<CsvViewer
  source={{
    kind: "blob",
    blob: file,
    identityKey: `file:${file.name}:${file.size}:${file.lastModified}`,
    fileName: file.name,
    mimeType: file.type,
  }}
/>

API Reference

PropTypeDescription
sourceCsvViewerSourceCanonical CSV/TSV source. URL sources stream from the response body when possible; Blob sources parse in a worker when available; text sources parse synchronously; table sources render pre-parsed { columns, rows } without parsing.
dialectCsvDialectDelimiter and header semantics. Defaults are inferred from source.fileName or source MIME type when possible, otherwise CSV with a header row.
classNamestringOptional class on the viewer container.
controlsbooleanShow the row/column count, zoom controls, and download action. Defaults to true.
heightnumberScroll viewport height in pixels. Defaults to 480. Ignored when fillHeight.
fillHeightbooleanFlex the body to fill the parent's height instead of a fixed height (the parent must give a definite height). Defaults to false.
activeCell{ rowIndex: number; columnIndex: number } | nullA cell to highlight (0-based source row + source column among data columns).
isolateStylesbooleanRender 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.

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.

Source

csv-viewer.tsx