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| Name | Role | Status | |
|---|---|---|---|
| Ava Thompson | ava.t@example.com | Admin | Active |
| Liam Chen | liam.chen@example.com | Editor | Active |
| Sofia Patel | sofia.p@example.com | Viewer | Inactive |
| Noah Garcia | noah.g@example.com | Editor | Active |
| Mia Johnson | mia.j@example.com | Admin | Pending |
| Ethan Kim | ethan.kim@example.com | Viewer | Active |
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.
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.
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.
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.
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.
<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>