useExport
Downloads rows as CSV, Excel or PDF. Neither heavy library reaches your bundle until someone actually exports.
<script setup lang="ts">
import type { ColumnDef } from '@shared/components/common/DataTable/types'
import { useExport } from '@shared/composables/useExport'
import { Button } from '@shared/components/ui/button'
import { formatCurrency, formatDate } from '@shared/utils/format'
interface Invoice extends Record<string, unknown> {
reference: string
customer: string
issued: Date
total: number
}
const rows: Invoice[] = [
{ reference: 'INV-2026-0418', customer: 'Northwind Traders', issued: new Date(2026, 4, 12), total: 4820 },
{ reference: 'INV-2026-0419', customer: 'Contoso Ltd', issued: new Date(2026, 4, 14), total: 1140.5 },
{ reference: 'INV-2026-0420', customer: 'Fabrikam', issued: new Date(2026, 4, 19), total: 12300 },
]
const columns: ColumnDef<Invoice>[] = [
{ key: 'reference', label: 'Reference' },
{ key: 'customer', label: 'Customer' },
{ key: 'issued', label: 'Issued', render: value => formatDate(value as Date) },
{ key: 'total', label: 'Total', render: value => formatCurrency(Number(value), 'USD') },
]
function shown(row: Invoice, column: ColumnDef<Invoice>): string {
const value = row[column.key]
return column.render ? String(column.render(value, row)) : String(value)
}
const { exportCSV, exportExcel, exportPDF } = useExport<Invoice>()
</script>
<template>
<div class="flex flex-col gap-4">
<table class="w-full border-collapse sa-text-sm">
<thead>
<tr class="border-b border-border text-start">
<th v-for="column in columns" :key="column.key" class="py-2 text-start font-medium">
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.reference" class="border-b border-border">
<td v-for="column in columns" :key="column.key" class="py-2">
{{ shown(row, column) }}
</td>
</tr>
</tbody>
</table>
<div class="flex flex-wrap gap-2">
<Button size="sm" variant="outline" @click="exportCSV(rows, columns, 'invoices')">
Download CSV
</Button>
<Button size="sm" variant="outline" @click="exportExcel(rows, columns, 'invoices')">
Download Excel
</Button>
<Button size="sm" variant="outline" @click="exportPDF(rows, columns, 'invoices')">
Download PDF
</Button>
</div>
</div>
</template>Import
import { useExport } from '@shared/composables/useExport'Signature
function useExport<T extends Record<string, unknown>>(): {
exportCSV: (data: T[], columns: ColumnDef<T>[], filename: string) => void
exportExcel: (data: T[], columns: ColumnDef<T>[], filename: string) => Promise<void>
exportPDF: (data: T[], columns: ColumnDef<T>[], filename: string) => Promise<void>
}Returns
| Member | Type | Produces | Library |
|---|---|---|---|
exportCSV | synchronous | {filename}.csv | None. Written by hand. |
exportExcel | Promise<void> | {filename}.xlsx | xlsx, lazy-loaded. |
exportPDF | Promise<void> | {filename}.pdf | jspdf and jspdf-autotable, lazy-loaded. |
filename carries no extension — each function adds its own.
Each takes the columns you want in the file, so a reader can export a subset. ColumnDef supplies key for the value, label for the header and, when it has one, render for what the cell shows.
What lands in the file
| CSV | Excel | ||
|---|---|---|---|
| Headers | The label in the active language | The label in the active language | The label in English |
| Cells | What the table shows | What the table shows | What the table shows, in English |
| Numbers | Plain numbers | Number cells a spreadsheet can sum | Text |
A label can be an i18n key or plain text. A key is resolved, so the header reads Name rather than users.fields.name.
A cell is exported as its column renders it. When the column has a render, the file gets that text: an option label in the active language instead of active, and the local date the table shows instead of an ISO timestamp. A column with no render exports its value as it is. A number value stays a number in CSV and Excel, so a total can be summed.
The PDF prints headers and cells in the base language, English. jsPDF's built-in font has no Arabic glyphs, so a translated PDF would print broken characters. Embed a Unicode font with doc.addFont if you need every language in the PDF.
Example
<script setup lang="ts">
import type { ColumnDef } from '@shared/components/common/DataTable/types'
import { useExport } from '@shared/composables/useExport'
import { formatCurrency } from '@shared/utils/format'
interface Invoice extends Record<string, unknown> {
reference: string
customer: string
total: number
}
const rows: Invoice[] = [
{ reference: 'INV-2026-0418', customer: 'Northwind', total: 4820 },
{ reference: 'INV-2026-0419', customer: 'Contoso', total: 1140.5 },
]
const columns: ColumnDef<Invoice>[] = [
{ key: 'reference', label: 'Reference' },
{ key: 'customer', label: 'Customer' },
{ key: 'total', label: 'Total', render: value => formatCurrency(Number(value), 'USD') },
]
const { exportCSV, exportExcel, exportPDF } = useExport<Invoice>()
</script>
<template>
<div class="flex flex-wrap gap-2">
<button type="button" @click="exportCSV(rows, columns, 'invoices')">
CSV
</button>
<button type="button" @click="exportExcel(rows, columns, 'invoices')">
Excel
</button>
<button type="button" @click="exportPDF(rows, columns, 'invoices')">
PDF
</button>
</div>
</template>Notes
- The CSV is written by hand, and every cell goes through
JSON.stringify. That quotes and escapes it, so a comma, a quote or a newline inside a value cannot break the file. - It starts with a UTF-8 byte-order mark, because without one Excel opens a UTF-8 CSV as the system code page and mangles every accented name.
xlsxandjspdfare imported inside their exporters, so a project that never exports never downloads them.- A missing value is written as an empty string rather than
undefined. xlsxinstalls from SheetJS's own CDN,https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz, not from the npm registry, whose copy stopped at 0.18.5. 0.20.3 carries the fixes for both publishedxlsxsecurity reports.npm auditdoes not check a dependency installed from a URL, so watchcdn.sheetjs.comfor new versions yourself.- The DataTable's export menu and the export button appear only while
config.features.exportSystemis on. The composable itself does not read the flag. - The download is triggered by a temporary anchor, and the object URL is revoked straight after, so nothing is left holding the blob.
Related
- Export button — the dashboard action.
- Export column picker — choosing the columns.
- DataTable — the table's own export section.