A data grid CSV export JavaScript feature looks like a button until users depend on it for monthly reporting, reconciliation, or a customer escalation. Then the real questions arrive: Does it export filtered rows? Does it preserve the column order users chose? Why did a ZIP code lose its leading zero? Why did a spreadsheet interpret a value as a formula?
A useful export is not a dump of whatever happens to be rendered in the DOM. It is a deliberate representation of the grid state and the business data behind it. Get that contract right early, and CSV export becomes a dependable part of the product instead of a recurring source of support tickets.
Define what the CSV should represent
Before writing a serializer, decide which version of the data users expect. A grid commonly has raw source records, filtered records, sorted records, paginated records, selected records, and rows currently visible in a virtualized viewport. Those are not interchangeable.
For most reporting and operations screens, the expected default is the current result set: active filters and sort order applied, but all matching rows included regardless of pagination or virtual scrolling. Exporting only the 50 rows on the current page is sometimes useful, but it should be labeled clearly as “Export current page.” Exporting only rendered rows is almost always wrong because virtual scrolling intentionally keeps most rows out of the DOM.
Selection creates another valid mode. If a user has checked 12 invoices from a filtered list of 3,000, “Export selected” should produce exactly those 12. A clean export UI makes the scope explicit rather than guessing:
- Export all filtered rows
- Export selected rows
- Export current page
- Export all rows, when permissions and data volume allow it
The grid already knows its active filters, sort state, column visibility, and selection. Your export should read from that state instead of recreating filtering rules in a separate utility. Duplicated state is how the screen says one thing while the downloaded file says another.
Build export columns from grid state
Users notice when a CSV includes columns they intentionally hid, excludes a pinned identifier they expected, or arrives in a different order from the grid. The export column model should therefore be derived from the current grid configuration, with a few intentional exceptions.
Internal IDs, action menus, selection checkboxes, and purely visual status indicators usually do not belong in a CSV. On the other hand, an identifier that is hidden only to save screen space may be important to an operations workflow. Make that an explicit per-column decision with export metadata such as `exportable`, `exportHeader`, and `exportValue`.
A minimal column configuration might look like this:
```ts type ExportColumn
const exportColumns = columns .filter((column) => column.visible && column.exportable) .map((column) => ({ header: column.header, getValue: column.getValue, })); ```
Do not export the text content of cells. Cells can contain buttons, badges, icons, truncated labels, custom renderers, or values formatted for display. Export from the row data through a column-aware formatter. That gives you a single place to convert a status code like `past_due` into “Past Due,” or a cents value into a dollar amount.
Serialize CSV correctly, not almost correctly
CSV has a small specification surface and a surprisingly large failure surface. A comma inside an address, a quote in a customer name, or a line break in a note can corrupt a file generated by a naive `join(',')` implementation.
Every field containing a comma, quote, carriage return, or newline must be wrapped in quotes. Quotes within the value must be doubled. Convert nullish values to an empty string, and standardize dates before serialization rather than allowing browser locale differences to leak into reports.
```ts function toCsvField(value: unknown): string { if (value === null || value === undefined) return '';
const text = String(value); const escaped = text.replace(/"/g, '""');
return /[",\r\n]/.test(escaped) ? `"${escaped}"` : escaped; }
function createCsv
return [header, ...body].join('\r\n'); } ```
The `\r\n` line ending is a practical choice for spreadsheet compatibility. For users who regularly open exports in Excel, a UTF-8 byte order mark can also help Excel recognize non-ASCII characters correctly. It is not universally required, so test it with the spreadsheet tools your customers actually use.
Formatting needs product judgment. Human-friendly values are usually right for reporting: “Jan 15, 2026,” “$1,234.50,” and “Active.” Machine-oriented exports may need ISO dates, unformatted decimal values, and stable enum codes. If your application serves both workflows, offer named export presets instead of making one file do two incompatible jobs.
Treat spreadsheet formula injection as an input concern
A CSV is text, but many spreadsheet applications interpret cells beginning with `=`, `+`, `-`, or `@` as formulas. If user-controlled text is exported, that behavior can turn a pasted customer name, support note, or product title into a spreadsheet formula when another person opens the file.
The appropriate mitigation depends on your risk model and intended consumers. Many applications prefix risky text fields with a single apostrophe during export, causing spreadsheet tools to treat the value as text. Apply it to fields that can contain untrusted content, after making a conscious decision about whether a leading minus sign should remain numeric in a particular column.
```ts function protectSpreadsheetText(value: unknown): string { const text = value == null ? '' : String(value); return /^[=+\-@]/.test(text) ? `'${text}` : text; } ```
Do not use a blanket transformation without considering column types. A negative balance is legitimate data, while a customer-entered comment beginning with `=` deserves protection. Column-level formatters make that distinction manageable.
Download in the browser without leaking resources
For modest result sets already available in the client, the browser can generate and download a CSV without a server round trip. Create a Blob with a CSV MIME type, create an object URL, trigger a temporary anchor, then revoke the URL.
```ts function downloadCsv(filename: string, csv: string) { const blob = new Blob([`\uFEFF${csv}`], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a');
anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } ```
Use predictable filenames that help users find files later, such as `open-invoices-2026-07-20.csv`. Include the report context, not an opaque internal route name.
Client-side export is fast to implement and keeps the interaction local. It is not the right answer for every dataset. If filtering depends on server-side queries, permissions, calculated fields, or millions of rows, the server should generate the export from the same authorized query definition that powers the grid. That avoids fetching a huge result set into the browser and prevents a client from requesting records it should not see.
Make large exports feel intentional
A 500-row export can happen immediately. A 500,000-row export needs a different experience. Generating a large CSV on the main thread can freeze the grid, and holding every row plus one giant string in memory can be expensive.
For larger client-side jobs, consider a Web Worker and chunked processing. For truly large or expensive reports, create an asynchronous server-side export job, show progress when possible, and notify the user when the file is ready. Set a reasonable export limit when the query would be too costly, then explain the next action: narrow filters, choose a date range, or request a scheduled report.
This is also where auditability matters. A finance or admin product may need to record who exported which report, when, and with which filters. A server-side pipeline is often the better fit for that requirement.
Test the cases users will actually find
CSV export deserves targeted tests because the common happy path proves very little. Test commas, quotes, embedded line breaks, empty values, emoji, accents, long identifiers, dates, negative amounts, and values that start with formula characters. Confirm that filters, sorting, selected rows, hidden columns, reordered columns, and pagination each affect export exactly as the UI promises.
Also test the file in the spreadsheet software your customers use. A valid CSV that opens with broken characters or shifted columns in a common tool is still a failed feature from the user's perspective.
A complete grid reduces the wiring here. Simple Table includes CSV export alongside filtering, sorting, selection, column controls, pagination, virtual scrolling, and TypeScript definitions, so export behavior can follow grid state instead of becoming another disconnected utility.
The best export button earns trust quietly. A user applies a filter, rearranges the columns, downloads a file, and finds exactly the data they were looking at, in a form they can use immediately. That is a small interaction with a large effect on whether your application feels ready for serious work.
