Nasaq

ExportButton

A button or menu that exports table data as CSV, XLSX or JSON, and PDF through a callback, with column choice, a rows scope of selected, filtered or all, a progress state and pure serializers with no dependency.

PreviewOpen ↗

Code

import { ExportButton, type ExportColumn } from "@fadymondy/nasaq/web";type Row = { id: string; name: string; email: string };const columns: ExportColumn<Row>[] = [  { id: "name", label: "Name" },  { id: "email", label: "Email" },];export function Toolbar({ all, filtered, selected }: { all: Row[]; filtered: Row[]; selected: Row[] }) {  return <ExportButton columns={columns} scopes={{ selected, filtered, all }} filename="contacts" />;}

Files · beta

Live examples and controls: ExportButton in the lab.

Install

npx shadcn@latest add https://docs.nasaqui.com/r/export-action.json

The "Export" action of a list or a report. It opens a dialog where the user picks a format, which rows (the selection, the current filter or everything) and which columns, then builds the file in the browser with a progress state and a cancel button. The serializers are pure functions you can also use without the UI.

When to use

  • Letting people take table data out as CSV, Excel or JSON.
  • Exporting rows that live on the server: give a scope a load function.
  • Adding PDF by handing the component your own PDF builder.

When not to use

  • Downloading one known file: use a plain link or a Button.
  • Sharing a link or inviting people: use ShareButton.
  • Printing a page: call window.print() from a PageActions button.

Import

import { ExportButton, toCsv, toXlsx } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"

Quick start

import { ExportButton, type ExportColumn } from "@fadymondy/nasaq/web";

type Row = { id: string; name: string; email: string };

const columns: ExportColumn<Row>[] = [
  { id: "name", label: "Name" },
  { id: "email", label: "Email" },
];

export function Toolbar({ all, filtered, selected }: { all: Row[]; filtered: Row[]; selected: Row[] }) {
  return <ExportButton columns={columns} scopes={{ selected, filtered, all }} filename="contacts" />;
}

Anatomy

ExportButton                  data-slot="export-button"    (or a menu when mode="menu")
└─ ExportDialog               data-slot="export-dialog"
   ├─ format cards            CSV, Excel, JSON (+ PDF with onExportPdf)
   ├─ rows scope              radios with row counts
   ├─ columns                 checkboxes with select all
   ├─ progress                data-slot="export-progress"  Progress + Cancel
   └─ result                  data-slot="export-done"      Download again / Done

API

ExportButton (ExportActionProps<T>)

PropTypeDefaultDescription
columnsExportColumn<T>[]{ id, label, value? }. value(row) gives the cell, else row[id].
scopes{ selected?, filtered?, all? }Each is T[] or { count, load() } for server rows. A scope with no rows is disabled; a missing scope is hidden.
formats("csv" | "xlsx" | "json" | "pdf")[]csv, xlsx, jsonpdf only appears when onExportPdf is given.
defaultFormat / defaultScopefirst availableStarting choices.
filename / sheetNamestring"export"File name without extension; XLSX sheet name.
onExportPdf(request, { signal, onProgress }) => Promise<Blob | void>Builds the PDF. Return a Blob to download it, or nothing if you delivered it yourself.
onDownload(file) => void | Promisebrowser downloadReplace the download (save dialog, upload to storage).
onComplete(file) => voidCalled after the file is handed over.
mode"dialog" | "menu""dialog"menu lists formats that export at once, plus "Export options…".
children / variant / size / disabled / classNameExport, secondary, smThe trigger button.
labelsPartial<ExportActionLabels>Override any string.

ExportDialog takes the same props plus open and onOpenChange, for opening it from your own control.

Serializers (pure, no DOM): toCsv(rows, { delimiter, newline, bom, guardFormulas }), csvField, toJson(records), toXlsx(rows, { sheetName, rtl }), zipStore, crc32, buildExportFile({ format, columns, rows, onProgress, signal }).

Examples

Server rows behind a scope

<ExportButton
  columns={columns}
  scopes={{ all: { count: 12840, load: () => api.listAllContacts() } }}
  filename="all-contacts"
/>

PDF through a callback

<ExportButton
  columns={columns}
  scopes={{ all: rows }}
  onExportPdf={async (request, { signal, onProgress }) => {
    const res = await fetch("/api/pdf", { method: "POST", body: JSON.stringify(request.rows), signal });
    onProgress(1);
    return res.blob();
  }}
/>

CSV outside the UI

import { toCsv } from "@fadymondy/nasaq/web";

const csv = toCsv([["Name", "Note"], ["Sara", "=1+1"]], { bom: true }); // "=1+1" is exported as text

Accessibility

  • The dialog traps focus and closes with Escape (not while a file is being built; use Cancel).
  • Formats and rows are radio groups with their own legends; columns are checkboxes with a select-all that shows the mixed state.
  • The progress bar has an accessible name; the result line is a role="status" live region.
  • Formula-like text (=, +, -, @) is prefixed so a spreadsheet does not run it.

RTL & i18n

  • Strings ship in English and Arabic. CSV gets a UTF-8 BOM so Excel reads Arabic; XLSX sheets are right-to-left in Arabic.
  • File names and counts stay left-to-right inside <bdi dir="ltr">; numbers follow the locale.

Styling & tokens

  • Built from Button, Dialog, RadioGroup, Checkbox, Progress and Alert; use their tokens. Extend with className.

Do / Don't

  • Do give value for columns whose cell is not a plain field (dates, joined tags).
  • Do use load for large or remote scopes so the rows are fetched only when chosen.
  • Don't pass rendered React nodes as cell values; return text or numbers.
  • Don't rely on PDF without onExportPdf.

Lab

https://docs.nasaqui.com/?path=/docs/components-files-export-button--docs

On this page