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.
"use client"
import * as React from "react"
import { SAMPLE_CSV } from "@/components/demos/document-fixtures"
import { CsvViewer } from "mischief-ui/csv-viewer"
export function CsvViewerDemo() {
const [csv, setCsv] = React.useState<string>()
React.useEffect(() => {
const controller = new AbortController()
void fetch(SAMPLE_CSV, { signal: controller.signal })
.then((response) => response.text())
.then((text) => setCsv(text))
.catch(() => undefined)
return () => controller.abort()
}, [])
return <CsvViewer className="w-full max-w-2xl" source={csv} maxRows={6} />
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/csv-viewerimport { CsvViewer } from "mischief-ui/csv-viewer"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"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 }
}}
/>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.