Mischief

34 / Documents

CSV Viewer

A real table for delimited data, with sortable columns, a sticky header, and a row cap so a large file cannot lock the page.

Installation

Copy the source into your project, or keep it behind a package.

npx shadcn@latest add Tinkerers-Labs/mischief-ui/csv-viewer
import { CsvViewer } from "mischief-ui/csv-viewer"
Also installs
  • papaparse
  • lucide-react

Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.

registry/default/csv-viewer/csv-viewer.tsx
"use client" import * as React from "react"import { ArrowDown, ArrowUp, TriangleAlert } from "lucide-react"import { cn } from "@/lib/utils" export type CsvTable = {  fields: string[]  rows: string[][]} export type CsvParser = (source: string | File) => Promise<CsvTable> export type CsvViewerProps = Omit<

Usage

export function Preview({ file }: { file: File }) {
  return <CsvViewer source={file} maxRows={200} />
}

Parsing

papaparse is an optional peer, imported the first time a source is parsed and never bundled for anyone who does not open a CSV. Without it installed, and without a parser of your own, the component says so rather than failing quietly.

Pass table when the data is already parsed -- from your API, from a worker, from a database -- and no parser is involved at all. Pass parser to use something else, or to parse somewhere that will not block the page.

<CsvViewer
  source={file}
  parser={async (input) => {
    const { fields, rows } = await parseInWorker(input)
    return { fields, rows }
  }}
/>
A parser returns { fields, rows }; how it gets there is up to you.

Delimiters, quoting, and encoding are the parser's business, not the viewer's. papaparse detects the common ones; a file that needs a fixed delimiter or a particular encoding is a good reason to pass your own.

Large files

Every row given to the component is rendered. maxRows caps what is shown, which keeps a large file from putting hundreds of thousands of cells into the page, and is the difference between a preview that opens instantly and a tab that stops responding.

Treat this as a preview of a file rather than a spreadsheet. When someone needs to work through all of it, page or virtualise on your side and hand the viewer one page at a time.

API

sourcestring | FileCSV text or a file to parse.
tableCsvTableAlready parsed data as fields and rows. Skips the parser entirely.
parser(source) => Promise<CsvTable>Replaces the default parser. Supply this and papaparse is never loaded.
maxRowsnumberHow many rows to render. Defaults to 200.
emptyLabel, loadingLabelReactNodeCopy for those two states.

CsvTable

fieldsstring[]Column headers, in order.
rowsstring[][]Cells per row, aligned to fields.

Accessibility

The data is a real table with a caption, column headers, and aria-sort on the sorted column, so it can be navigated with table commands rather than read as a wall of text. Sorting is a button inside each header. Numeric columns sort numerically instead of as text. When rows are capped the footer says how many of the total are shown, rather than silently truncating.