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.
ID | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| Ava | Thompson | Owner | Engineering | active | 22 | $45,000 | Jan 2, 2023 | 0% | |||
| Liam | Nguyen | Admin | Marketing | inactive | 25 | $47,137 | Jan 13, 2023 | 9% | |||
| Noah | Silva | Editor | Design | pending | 28 | $49,274 | Jan 24, 2023 | 18% | |||
| Emma | Carter | Viewer | Sales | active | 31 | $51,411 | Feb 4, 2023 | 27% | |||
| Olivia | Rossi | Owner | Support | inactive | 34 | $53,548 | Feb 15, 2023 | 36% | |||
| William | Walker | Admin | Engineering | pending | 37 | $55,685 | Feb 26, 2023 | 45% | |||
| Sophia | Patel | Editor | Marketing | active | 40 | $57,822 | Mar 9, 2023 | 54% | |||
| James | Muller | Viewer | Design | inactive | 43 | $59,959 | Mar 20, 2023 | 63% | |||
| Isabella | Park | Owner | Sales | pending | 46 | $62,096 | Mar 31, 2023 | 72% | |||
| Lucas | Reyes | Admin | Support | active | 49 | $64,233 | Apr 11, 2023 | 81% | |||
| $4,120,536 |
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_sheet → XLSX.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.