Shadcn React Table

Search documentation

Search the docs

Export

The table ships no built-in export — the same stance as Material React Table — so the block carries no CSV or spreadsheet dependencies. Everything an exporter needs is already on the table instance: pick the rows, map them through the visible columns, and hand the result to whichever writer you prefer.

Export
GitHub
ID
AvaThompsonOwnerEngineeringactive22$45,000Jan 2, 2023
0%
LiamNguyenAdminMarketinginactive25$47,137Jan 13, 2023
9%
NoahSilvaEditorDesignpending28$49,274Jan 24, 2023
18%
EmmaCarterViewerSalesactive31$51,411Feb 4, 2023
27%
OliviaRossiOwnerSupportinactive34$53,548Feb 15, 2023
36%
WilliamWalkerAdminEngineeringpending37$55,685Feb 26, 2023
45%
SophiaPatelEditorMarketingactive40$57,822Mar 9, 2023
54%
JamesMullerViewerDesigninactive43$59,959Mar 20, 2023
63%
IsabellaParkOwnerSalespending46$62,096Mar 31, 2023
72%
LucasReyesAdminSupportactive49$64,233Apr 11, 2023
81%
$4,120,536
Rows per page
1–10 of 48

Reading the data

Pick a row scope from the table's row models, then build an array-of-arrays (header + rows) from the visible data columns:

import type { DataTableInstance } from "@/components/ui/data-table"

function toRows<TData>(table: DataTableInstance<TData>) {
  // Selected rows when any are selected, else the filtered set (all pages).
  const selected = table.getSelectedRowModel().rows
  const rows = selected.length > 0 ? selected : table.getFilteredRowModel().rows
  // Data columns only — injected columns (selection, actions, …) have no accessor.
  const columns = table
    .getVisibleLeafColumns()
    .filter((column) => column.accessorFn != null)
  return [
    columns.map((column) => column.id),
    ...rows.map((row) => columns.map((column) => row.getValue(column.id))),
  ]
}

Other scopes: table.getRowModel().rows (current page) and table.getPreFilteredRowModel().rows (everything, ignoring filters).

Writing a CSV

No library needed for CSV. Two details matter: quote cells containing delimiters, and neutralize formula injection (spreadsheets execute cells starting with =, +, -, @, tab, or CR when a CSV is opened):

function toCsv(rows: unknown[][]) {
  const escape = (value: unknown) => {
    const text = value == null ? "" : String(value)
    const guarded = /^[=+\-@\t\r]/.test(text) ? `'${text}` : text
    return /[",\n]/.test(guarded)
      ? `"${guarded.replace(/"/g, '""')}"`
      : guarded
  }
  return rows.map((row) => row.map(escape).join(",")).join("\n")
}

function downloadCsv(csv: string, fileName: string) {
  // The BOM makes Excel detect UTF-8.
  const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" })
  const url = URL.createObjectURL(blob)
  const link = document.createElement("a")
  link.href = url
  link.download = `${fileName}.csv`
  link.click()
  URL.revokeObjectURL(url)
}

Wire it to the toolbar with renderToolbarActions:

const table = useDataTable({
  data,
  columns,
  enableRowSelection: true,
  renderToolbarActions: ({ table }) => (
    <Button onClick={() => downloadCsv(toCsv(toRows(table)), "users")}>
      Export CSV
    </Button>
  ),
})

Excel and richer formats

For .xlsx, feed the same array-of-arrays to SheetJS (XLSX.utils.aoa_to_sheetXLSX.writeFile) — note SheetJS is installed from their CDN tarball, not npm. For heavier CSV needs (custom delimiters, streaming), PapaParse's unparse accepts the same shape.