Skip to content

Export ​

CSV (built-in) ​

ts
grid.value.exportCsv({ fileName: "employees.csv" });   // download
const csv = grid.value.getDataAsCsv();                  // string
ParameterDefaultDescription
fileName"export.csv"
separator","use ";" for German Excel
includeHeaderstrue
onlySelectedfalseonly the selected rows
formattedtrueformatted values instead of raw values
columnsvisible columnscolumn ids to export
includeGroupsfalseinclude group rows

All filtered rows are exported (all pages, including collapsed groups and tree rows) in the current sort order. The file contains a byte order mark, so Excel detects UTF-8.

CSV options ​

Use the options when the file is imported by another program: a semicolon separator for Excel with German regional settings, raw values (formatted: false) for machines instead of humans, or only the rows the user has checked. The preview below shows api.getDataAsCsv() with the chosen options – sort, filter or select rows and watch it change.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label>
        Separator
        <select v-model="separator">
          <option value=",">comma</option>
          <option value=";">semicolon</option>
          <option :value="'\t'">tab</option>
        </select>
      </label>
      <label>
        Columns
        <select v-model="columnSet">
          <option value="all">all visible</option>
          <option value="name">name and salary</option>
        </select>
      </label>
      <label><input v-model="onlySelected" type="checkbox" /> Only selected</label>
      <label><input v-model="formatted" type="checkbox" /> Formatted</label>
      <label><input v-model="includeHeaders" type="checkbox" /> Headers</label>
      <button @click="api?.exportCsv({ ...params, fileName: 'employees.csv' })">Download</button>
    </div>
    <Datagrid
      :columns="columns"
      :rows="rows"
      :options="options"
      @ready="api = $event"
      @state-change="updatePreview"
    />
    <pre class="csvopt-preview">{{ preview }}</pre>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, shallowRef, watch } from "vue";
import {
  Datagrid,
  formatters,
  type ColumnConfig,
  type Core,
  type CsvExportParams,
  type GridOptions,
} from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = createEmployees(50);
const api = shallowRef<Core>();
const separator = ref(",");
const columnSet = ref<"all" | "name">("all");
const onlySelected = ref(false);
const formatted = ref(true);
const includeHeaders = ref(true);
const preview = ref("");

const params = computed<CsvExportParams>(() => ({
  separator: separator.value,
  onlySelected: onlySelected.value,
  formatted: formatted.value,
  includeHeaders: includeHeaders.value,
  // undefined = all visible columns
  columns: columnSet.value === "name" ? ["firstName", "lastName", "salary"] : undefined,
}));

// shows the first lines of the csv text
function updatePreview() {
  if (!api.value) return;
  const lines = api.value.getDataAsCsv(params.value).split("\r\n");
  const more = lines.length > 6 ? `\n… ${lines.length - 6} more lines` : "";
  preview.value = lines.slice(0, 6).join("\n") + more;
}

watch([api, params], updatePreview);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 70 },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", valueFormatter: formatters.currency("EUR") },
  { field: "startDate", text: "Start date", type: "date", valueFormatter: formatters.date() },
];

const options: GridOptions<Employee> = {
  rowId: (data) => data.id,
  checkboxSelection: true,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>

<style>
.csvopt-preview {
  margin: 0;
  max-height: 130px;
  overflow: auto;
  padding: 6px 8px;
  border-radius: 6px;
  background: var(--vp-c-bg-soft);
  font-family: var(--vp-font-family-mono);
  font-size: 12px;
  line-height: 1.5;
  tab-size: 12;
}
</style>
tsx
import { useCallback, useEffect, useMemo, useState } from "react";
import {
  Datagrid,
  formatters,
  type ColumnConfig,
  type Core,
  type CsvExportParams,
  type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(50);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "id", text: "ID", type: "number", width: 70 },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  { field: "salary", text: "Salary", type: "number", valueFormatter: formatters.currency("EUR") },
  { field: "startDate", text: "Start date", type: "date", valueFormatter: formatters.date() },
];

const options: GridOptions<Employee> = {
  rowId: (data) => data.id,
  checkboxSelection: true,
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function ExportCsvOptions() {
  const [api, setApi] = useState<Core>();
  const [separator, setSeparator] = useState(",");
  const [columnSet, setColumnSet] = useState<"all" | "name">("all");
  const [onlySelected, setOnlySelected] = useState(false);
  const [formatted, setFormatted] = useState(true);
  const [includeHeaders, setIncludeHeaders] = useState(true);
  const [preview, setPreview] = useState("");

  const params = useMemo<CsvExportParams>(
    () => ({
      separator,
      onlySelected,
      formatted,
      includeHeaders,
      // undefined = all visible columns
      columns: columnSet === "name" ? ["firstName", "lastName", "salary"] : undefined,
    }),
    [separator, columnSet, onlySelected, formatted, includeHeaders]
  );

  // shows the first lines of the csv text
  const updatePreview = useCallback(() => {
    if (!api) return;
    const lines = api.getDataAsCsv(params).split("\r\n");
    const more = lines.length > 6 ? `\n… ${lines.length - 6} more lines` : "";
    setPreview(lines.slice(0, 6).join("\n") + more);
  }, [api, params]);

  useEffect(updatePreview, [updatePreview]);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          Separator{" "}
          <select value={separator} onChange={(e) => setSeparator(e.target.value)}>
            <option value=",">comma</option>
            <option value=";">semicolon</option>
            <option value={"\t"}>tab</option>
          </select>
        </label>
        <label>
          Columns{" "}
          <select value={columnSet} onChange={(e) => setColumnSet(e.target.value as "all" | "name")}>
            <option value="all">all visible</option>
            <option value="name">name and salary</option>
          </select>
        </label>
        <label>
          <input type="checkbox" checked={onlySelected} onChange={(e) => setOnlySelected(e.target.checked)} /> Only
          selected
        </label>
        <label>
          <input type="checkbox" checked={formatted} onChange={(e) => setFormatted(e.target.checked)} /> Formatted
        </label>
        <label>
          <input type="checkbox" checked={includeHeaders} onChange={(e) => setIncludeHeaders(e.target.checked)} />{" "}
          Headers
        </label>
        <button onClick={() => api?.exportCsv({ ...params, fileName: "employees.csv" })}>Download</button>
      </div>
      <Datagrid columns={columns} rows={rows} options={options} onReady={setApi} onStateChange={updatePreview} />
      <pre className="csvopt-preview">{preview}</pre>
      <style>{`
        .csvopt-preview {
          margin: 0;
          max-height: 130px;
          overflow: auto;
          padding: 6px 8px;
          border-radius: 6px;
          background: var(--vp-c-bg-soft);
          font-family: var(--vp-font-family-mono);
          font-size: 12px;
          line-height: 1.5;
          tab-size: 12;
        }
      `}</style>
    </div>
  );
}

Grouped rows ​

By default only data rows are exported, also the rows of collapsed groups. includeGroups: true adds a line for every group row with the group key, the row count and the aggregated values – useful for reports with subtotals.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label><input v-model="includeGroups" type="checkbox" /> Include group rows</label>
      <button @click="api?.exportCsv({ includeGroups, fileName: 'salaries.csv' })">Download</button>
    </div>
    <Datagrid
      :columns="columns"
      :rows="rows"
      :options="options"
      v-model:columnGroups="groups"
      @ready="api = $event"
      @state-change="updatePreview"
    />
    <pre class="csvgroups-preview">{{ preview }}</pre>
  </div>
</template>

<script setup lang="ts">
import { ref, shallowRef, watch } from "vue";
import { Datagrid, type ColumnConfig, type Core, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const rows = createEmployees(12);
const api = shallowRef<Core>();
const groups = ref(["department"]);
const includeGroups = ref(true);
const preview = ref("");

function updatePreview() {
  // all rows are exported, also the rows of collapsed groups
  preview.value = api.value?.getDataAsCsv({ includeGroups: includeGroups.value }) ?? "";
}

watch([api, includeGroups], updatePreview);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "department", text: "Department", groupable: true },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
];

const options: GridOptions<Employee> = {
  groupDefaultExpanded: 0,
  defaultColumn: { sortable: true, flex: 1 },
};
</script>

<style>
.csvgroups-preview {
  margin: 0;
  max-height: 150px;
  overflow: auto;
  padding: 6px 8px;
  border-radius: 6px;
  background: var(--vp-c-bg-soft);
  font-family: var(--vp-font-family-mono);
  font-size: 12px;
  line-height: 1.5;
}
</style>
tsx
import { useCallback, useEffect, useState } from "react";
import { Datagrid, type ColumnConfig, type Core, type GridOptions } from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(12);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "department", text: "Department", groupable: true },
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "salary", text: "Salary", type: "number", aggFunc: "sum" },
];

const options: GridOptions<Employee> = {
  groupDefaultExpanded: 0,
  defaultColumn: { sortable: true, flex: 1 },
};

export default function ExportCsvGroups() {
  const [api, setApi] = useState<Core>();
  const [groups, setGroups] = useState(["department"]);
  const [includeGroups, setIncludeGroups] = useState(true);
  const [preview, setPreview] = useState("");

  const updatePreview = useCallback(() => {
    // all rows are exported, also the rows of collapsed groups
    setPreview(api?.getDataAsCsv({ includeGroups }) ?? "");
  }, [api, includeGroups]);

  useEffect(updatePreview, [updatePreview]);

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          <input type="checkbox" checked={includeGroups} onChange={(e) => setIncludeGroups(e.target.checked)} />{" "}
          Include group rows
        </label>
        <button onClick={() => api?.exportCsv({ includeGroups, fileName: "salaries.csv" })}>Download</button>
      </div>
      <Datagrid
        columns={columns}
        rows={rows}
        options={options}
        columnGroups={groups}
        onColumnGroupsChange={setGroups}
        onReady={setApi}
        onStateChange={updatePreview}
      />
      <pre className="csvgroups-preview">{preview}</pre>
      <style>{`
        .csvgroups-preview {
          margin: 0;
          max-height: 150px;
          overflow: auto;
          padding: 6px 8px;
          border-radius: 6px;
          background: var(--vp-c-bg-soft);
          font-family: var(--vp-font-family-mono);
          font-size: 12px;
          line-height: 1.5;
        }
      `}</style>
    </div>
  );
}

Excel and PDF ​

Excel and PDF export are available in the separate package @datagrid/export. It uses exceljs and pdfmake, which are large, so load the package on demand:

sh
pnpm add @datagrid/export
Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <button @click="grid?.exportCsv({ fileName: 'employees.csv' })">CSV (built-in)</button>
      <button @click="toExcel">Excel</button>
      <button @click="toPdf">PDF</button>
    </div>
    <Datagrid ref="grid" :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createEmployees, type Employee } from "./data";

const grid = ref<InstanceType<typeof Datagrid>>();
const rows = createEmployees(100);

// the export package is loaded on demand, because exceljs and pdfmake are large
async function toExcel() {
  const { exportXlsx } = await import("@datagrid/export");
  await exportXlsx(grid.value!.core, { filename: "employees" });
}

async function toPdf() {
  const { exportPdf } = await import("@datagrid/export");
  exportPdf(grid.value!.core, { filename: "employees", pageOrientation: "landscape" });
}

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  {
    field: "salary",
    text: "Salary",
    type: "number",
    valueFormatter: formatters.currency("EUR"),
    // colors are exported to excel
    cellStyle: ({ value }) => (value > 100000 ? { backgroundColor: "#dafbe1" } : undefined),
  },
];

const options: GridOptions<Employee> = {
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>
tsx
import { useRef } from "react";
import {
  Datagrid,
  formatters,
  type ColumnConfig,
  type DatagridHandle,
  type GridOptions,
} from "@datagrid/react-ui";
import { createEmployees, type Employee } from "../data";

const rows = createEmployees(100);

const columns: Array<ColumnConfig<Employee>> = [
  { field: "firstName", text: "First name" },
  { field: "lastName", text: "Last name" },
  { field: "department", text: "Department" },
  {
    field: "salary",
    text: "Salary",
    type: "number",
    valueFormatter: formatters.currency("EUR"),
    // colors are exported to excel
    cellStyle: ({ value }) => (value > 100000 ? { backgroundColor: "#dafbe1" } : undefined),
  },
];

const options: GridOptions<Employee> = {
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function Export() {
  const grid = useRef<DatagridHandle>(null);

  // the export package is loaded on demand, because exceljs and pdfmake are large
  async function toExcel() {
    const { exportXlsx } = await import("@datagrid/export");
    await exportXlsx(grid.current!.api, { filename: "employees" });
  }

  async function toPdf() {
    const { exportPdf } = await import("@datagrid/export");
    exportPdf(grid.current!.api, { filename: "employees", pageOrientation: "landscape" });
  }

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <button onClick={() => grid.current?.exportCsv({ fileName: "employees.csv" })}>CSV (built-in)</button>
        <button onClick={toExcel}>Excel</button>
        <button onClick={toPdf}>PDF</button>
      </div>
      <Datagrid ref={grid} columns={columns} rows={rows} options={options} />
    </div>
  );
}

exportXlsx(api, options) ​

Creates an Excel table with filter buttons. Background colors, text colors and bold text of the cells (from cellClass and cellStyle) are exported. Numbers and dates are exported as values, everything else as formatted text.

OptionDescription
filenamefile name without extension
sheetNamename of the worksheet
fieldscolumn ids to export

Excel with conditional formatting ​

The Excel export reads the computed styles of every cell, so the colors you already use in the grid end up in the workbook: background colors (here from cellStyle by status), text colors and bold text (here from cellClass for overdue tasks and high priority). Use fields to export a subset of the columns, e.g. for a short list which is sent by mail.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <button @click="toExcel()">Excel (all columns)</button>
      <button @click="toExcel(['title', 'assignee', 'due'])">Excel (title, assignee, due)</button>
    </div>
    <Datagrid ref="grid" :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions } from "@datagrid/vue-ui";
import { createTasks, TODAY, type Task } from "./data";

const grid = ref<InstanceType<typeof Datagrid>>();
const rows = createTasks(40);

async function toExcel(fields?: Array<string>) {
  const { exportXlsx } = await import("@datagrid/export");
  // fields: column ids to export (default: all visible columns)
  await exportXlsx(grid.value!.core, { filename: "tasks", sheetName: "Tasks", fields });
}

const statusColors: Record<Task["status"], string> = {
  todo: "#eaeef2",
  "in progress": "#ddf4ff",
  review: "#fff8c5",
  done: "#dafbe1",
};

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", flex: 2 },
  { field: "assignee", text: "Assignee" },
  // background colors from cellStyle are exported
  { field: "status", text: "Status", cellStyle: ({ value }) => ({ backgroundColor: statusColors[value as Task["status"]] }) },
  // text color and bold text from css classes are exported too
  { field: "priority", text: "Priority", cellClass: ({ value }) => (value === "high" ? "xls-high" : undefined) },
  { field: "progress", text: "Progress", type: "number", valueFormatter: formatters.percent() },
  {
    field: "due",
    text: "Due",
    type: "date",
    valueFormatter: formatters.date(),
    cellClass: ({ data }) => (!data.done && data.due < TODAY ? "xls-overdue" : undefined),
  },
];

const options: GridOptions<Task> = {
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>

<style>
.xls-high {
  font-weight: bold;
}
.xls-overdue {
  color: #cf222e;
  font-weight: bold;
}
</style>
tsx
import { useRef } from "react";
import {
  Datagrid,
  formatters,
  type ColumnConfig,
  type DatagridHandle,
  type GridOptions,
} from "@datagrid/react-ui";
import { createTasks, TODAY, type Task } from "../data";

const rows = createTasks(40);

const statusColors: Record<Task["status"], string> = {
  todo: "#eaeef2",
  "in progress": "#ddf4ff",
  review: "#fff8c5",
  done: "#dafbe1",
};

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", flex: 2 },
  { field: "assignee", text: "Assignee" },
  // background colors from cellStyle are exported
  { field: "status", text: "Status", cellStyle: ({ value }) => ({ backgroundColor: statusColors[value as Task["status"]] }) },
  // text color and bold text from css classes are exported too
  { field: "priority", text: "Priority", cellClass: ({ value }) => (value === "high" ? "xls-high" : undefined) },
  { field: "progress", text: "Progress", type: "number", valueFormatter: formatters.percent() },
  {
    field: "due",
    text: "Due",
    type: "date",
    valueFormatter: formatters.date(),
    cellClass: ({ data }) => (!data.done && data.due < TODAY ? "xls-overdue" : undefined),
  },
];

const options: GridOptions<Task> = {
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function ExportExcelStyles() {
  const grid = useRef<DatagridHandle>(null);

  async function toExcel(fields?: Array<string>) {
    const { exportXlsx } = await import("@datagrid/export");
    // fields: column ids to export (default: all visible columns)
    await exportXlsx(grid.current!.api, { filename: "tasks", sheetName: "Tasks", fields });
  }

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <button onClick={() => toExcel()}>Excel (all columns)</button>
        <button onClick={() => toExcel(["title", "assignee", "due"])}>Excel (title, assignee, due)</button>
      </div>
      <Datagrid ref={grid} columns={columns} rows={rows} options={options} />
      <style>{`
        .xls-high {
          font-weight: bold;
        }
        .xls-overdue {
          color: #cf222e;
          font-weight: bold;
        }
      `}</style>
    </div>
  );
}

Numbers are exported as numbers without the number format of the grid (e.g. 0.45 instead of 45 %); dates stored as strings are exported as formatted text.

exportPdf(api, options) ​

OptionDescription
filenamefile name without extension
fieldscolumn ids to export
exportHeadersinclude headers (default true)
pageOrientation"portrait" or "landscape"
exportBehavior"download" (default), "open" or "print"
pdfHeaderStyle, pdfCellStylefunctions returning pdfmake styles for header and body cells
styles, defaultStyle, fonts, tableLayoutspassed to pdfmake

PDF with styles ​

pdfHeaderStyle and pdfCellStyle return pdfmake styles (bold, color, fillColor, alignment, fontSize, …) for each header and body cell. The cell callback receives the column (api) and the row (api) with its data, so the PDF can highlight the same values as the grid. exportBehavior: "open" opens the PDF in a new tab instead of downloading it, "print" opens the print dialog.

Source
vue
<template>
  <div class="demo">
    <div class="demo-toolbar">
      <label>
        <select v-model="orientation">
          <option value="portrait">Portrait</option>
          <option value="landscape">Landscape</option>
        </select>
      </label>
      <button @click="toPdf('download')">Download PDF</button>
      <button @click="toPdf('open')">Open in new tab</button>
      <button @click="toPdf('print')">Print</button>
    </div>
    <Datagrid ref="grid" :columns="columns" :rows="rows" :options="options" />
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Datagrid, formatters, type ColumnConfig, type GridOptions, type Row } from "@datagrid/vue-ui";
import { createTasks, TODAY, type Task } from "./data";

const grid = ref<InstanceType<typeof Datagrid>>();
const orientation = ref<"portrait" | "landscape">("portrait");
const rows = createTasks(60);

async function toPdf(exportBehavior: "download" | "open" | "print") {
  const { exportPdf } = await import("@datagrid/export");
  exportPdf(grid.value!.core, {
    filename: "tasks",
    exportBehavior,
    pageOrientation: orientation.value,
    defaultStyle: { fontSize: 9 },
    // pdfmake styles for the header cells
    pdfHeaderStyle: () => ({ bold: true, color: "#ffffff", fillColor: "#2e9e6e" }),
    // pdfmake styles for the body cells: right aligned numbers, red overdue dates
    pdfCellStyle: ({ api: column }, { api: row }) => {
      const task = (row as Row<Task>).data; // exported items are data rows
      if (column.id === "due" && !task.done && task.due < TODAY) {
        return { color: "#cf222e", bold: true };
      }
      return column.type.includes("number") ? { alignment: "right" } : undefined;
    },
  });
}

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", flex: 2 },
  { field: "assignee", text: "Assignee" },
  { field: "status", text: "Status" },
  { field: "estimate", text: "Estimate (h)", type: "number" },
  { field: "spent", text: "Spent (h)", type: "number" },
  { field: "due", text: "Due", type: "date", valueFormatter: formatters.date() },
];

const options: GridOptions<Task> = {
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};
</script>
tsx
import { useRef, useState } from "react";
import {
  Datagrid,
  formatters,
  type ColumnConfig,
  type DatagridHandle,
  type GridOptions,
  type Row,
} from "@datagrid/react-ui";
import { createTasks, TODAY, type Task } from "../data";

const rows = createTasks(60);

const columns: Array<ColumnConfig<Task>> = [
  { field: "title", text: "Task", flex: 2 },
  { field: "assignee", text: "Assignee" },
  { field: "status", text: "Status" },
  { field: "estimate", text: "Estimate (h)", type: "number" },
  { field: "spent", text: "Spent (h)", type: "number" },
  { field: "due", text: "Due", type: "date", valueFormatter: formatters.date() },
];

const options: GridOptions<Task> = {
  defaultColumn: { sortable: true, filterable: true, flex: 1 },
};

export default function ExportPdfStyles() {
  const grid = useRef<DatagridHandle>(null);
  const [orientation, setOrientation] = useState<"portrait" | "landscape">("portrait");

  async function toPdf(exportBehavior: "download" | "open" | "print") {
    const { exportPdf } = await import("@datagrid/export");
    exportPdf(grid.current!.api, {
      filename: "tasks",
      exportBehavior,
      pageOrientation: orientation,
      defaultStyle: { fontSize: 9 },
      // pdfmake styles for the header cells
      pdfHeaderStyle: () => ({ bold: true, color: "#ffffff", fillColor: "#2e9e6e" }),
      // pdfmake styles for the body cells: right aligned numbers, red overdue dates
      pdfCellStyle: ({ api: column }, { api: row }) => {
        const task = (row as Row<Task>).data; // exported items are data rows
        if (column.id === "due" && !task.done && task.due < TODAY) {
          return { color: "#cf222e", bold: true };
        }
        return column.type.includes("number") ? { alignment: "right" } : undefined;
      },
    });
  }

  return (
    <div className="demo">
      <div className="demo-toolbar">
        <label>
          <select value={orientation} onChange={(e) => setOrientation(e.target.value as "portrait" | "landscape")}>
            <option value="portrait">Portrait</option>
            <option value="landscape">Landscape</option>
          </select>
        </label>
        <button onClick={() => toPdf("download")}>Download PDF</button>
        <button onClick={() => toPdf("open")}>Open in new tab</button>
        <button onClick={() => toPdf("print")}>Print</button>
      </div>
      <Datagrid ref={grid} columns={columns} rows={rows} options={options} />
    </div>
  );
}

Released under the ISC License.