- Overview
- Anatomy
- Header
- Navigation
- Renderers
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.
"use client";
import * as React from "react";
import { CsvViewerDocument } from "@/components/ui/csv-viewer";
const FIRST = ["Jane", "John", "Amara", "Liu", "Diego", "Sofia", "Noah", "Mia"];
const LAST = [
"Doe",
"Smith",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/Blobassourceto 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.
"use client";
import * as React from "react";
import { CsvViewerDocument } from "@/components/ui/csv-viewer";
const FIRST = ["Jane", "John", "Amara", "Liu", "Diego", "Sofia", "Noah", "Mia"];
const LAST = [
"Doe",
"Smith",// 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
| Prop | Type | Description |
|---|---|---|
source | CsvViewerSource | Canonical 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. |
dialect | CsvDialect | Delimiter and header semantics. Defaults are inferred from source.fileName or source MIME type when possible, otherwise CSV with a header row. |
className | string | Optional class on the viewer container. |
controls | boolean | Show the row/column count, zoom controls, and download action. Defaults to true. |
height | number | Scroll viewport height in pixels. Defaults to 480. Ignored when fillHeight. |
fillHeight | boolean | Flex 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 } | null | A cell to highlight (0-based source row + source column among data columns). |
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. |
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
"use client";
import * as React from "react";
import { useKeyedMountEffect } from "@/hooks/use-keyed-mount-effect";
import type { CsvDialect, CsvTable } from "@/lib/csv";
import {
createViewerResource,
type ViewerResource,
} from "@/lib/viewer-resource";
import {