ShadTable
DiscordGitHub

ShadTable

A collection of composable table components built on shadcn/ui and TanStack Table.

Density & Export

One table, three modes: a compact/comfortable/spacious density toggle, CSV/Excel/PDF export built from whatever the table is currently showing, and a print-optimized view that ignores dark mode entirely.

npx shadcn add https://shadcn-table-library.vercel.app/r/density-export-table.json
Density
NameEmailRoleStatus
Ava Thompsonava.t@example.comAdminActive
Liam Chenliam.chen@example.comEditorActive
Sofia Patelsofia.p@example.comViewerInactive
Noah Garcianoah.g@example.comEditorActive
Mia Johnsonmia.j@example.comAdminPending
Ethan Kimethan.kim@example.comViewerActive

How it works

1.Density is three predefined class maps, not a runtime calculation

densityCell and densityHead are plain lookup objects keyed by 'compact' | 'comfortable' | 'spacious'. Switching density just picks a different precomputed set of Tailwind classes for every cell — no inline style math, no per-row measurement.

src/components/utility/data-table.tsx
const densityCell: Record<Density, string> = {
compact: 'py-1 text-xs',
comfortable: 'py-2 text-sm',
spacious: 'py-4 text-base',
}

2.Export reads whatever the table is currently showing, not a separate field list

getExportData() pulls headers straight from table.getHeaderGroups()[0].headers and row values from table.getRowModel().rows[].getVisibleCells(). CSV and Excel export always match the columns actually rendered — hide a column later and export follows automatically, with no second copy of the field list to keep in sync.

src/components/utility/data-table.tsx
function getExportData() {
const headers = table.getHeaderGroups()[0].headers.map((header) =>
typeof header.column.columnDef.header === 'string' ? header.column.columnDef.header : header.column.id,
)
const rows = table.getRowModel().rows.map((row) =>
row.getVisibleCells().map((cell) => String(cell.getValue() ?? '')),
)
return { headers, rows }
}

3.Excel export is an HTML table with an .xls extension, not a real spreadsheet file

toExcelHtml builds a plain <table> string, and downloadFile serves it with the application/vnd.ms-excel MIME type and a .xls filename. Excel (and Google Sheets) both recognize that combination and open it as a worksheet, so a real spreadsheet-writing library was never necessary for this example.

src/components/utility/export.ts
export function toExcelHtml(headers: string[], rows: string[][]): string {
const th = headers.map((h) => `<th>${h}</th>`).join('')
const trs = rows.map((row) => `<tr>${row.map((cell) => `<td>${cell}</td>`).join('')}</tr>`).join('')
return `<table><thead><tr>${th}</tr></thead><tbody>${trs}</tbody></table>`
}

4.PDF export is just the browser's print dialog

exportPdf calls window.print() directly — nothing else. "Save as PDF" is already a destination every modern browser offers in that dialog, so there's no PDF-generation code anywhere in this example, only the print: styling below that decides what the dialog actually shows.

src/components/utility/data-table.tsx
function exportPdf() {
window.print()
}

5.The print output ignores the app's theme entirely

Every table element carries a print: variant forcing black text on a white background and black borders, regardless of whether the page is currently in dark mode — a printed page is always on white paper. The density/export toolbar itself is print:hidden, since neither control has any reason to appear on a printed page.

src/components/utility/data-table.tsx
<div className="... print:hidden">{/* density + export controls */}</div>
<Table className="print:bg-white print:text-black">
{/* every TableHead / TableCell also carries print:text-black */}
</Table>