Starter recipe
Basic table
Start with typed local rows, formatting, sorting, filtering, editing, pagination, selection, and file operations.
Local dataTyped columnsEditing
Interactive, production component
tsx
import { useMemo, useState } from "react";
import type { z } from "zod";
import { DataTable } from "../../Datatable";
import type { ColumnSetting, ValidationContext } from "../../Datatable";
import { products, type ProductRow } from "./exampleData";
import type { ExampleProps } from "./types";
export const basicExampleXlsxAdapter = {
async download(...args: Parameters<import("../../Datatable").XlsxAdapter["download"]>) {
const { createXlsxAdapter } = await import("../../Datatable/xlsx");
return createXlsxAdapter().download(...args);
},
async parse(file: File) {
const { createXlsxAdapter } = await import("../../Datatable/xlsx");
return createXlsxAdapter().parse!(file);
},
} satisfies import("../../Datatable").XlsxAdapter;
export const disabledDemoSku = products[2]?.sku;
export const basicExampleColumns: ColumnSetting<ProductRow>[] = [
{
id: "id",
title: "ID",
order: 70,
width: 76,
align: "right",
headerAlign: "center",
filterType: "number",
editor: { type: "number", min: 1, max: 999_999, step: 1 },
validation: (zod: typeof z) =>
zod.coerce.number().int("Use a whole-number ID").min(1).max(999_999),
},
{
id: "sku",
title: "SKU",
order: 80,
width: 108,
editor: "text",
validation: (zod: typeof z) =>
zod.string().trim().regex(/^VB-\d{4,}$/, "Use a SKU like VB-1001"),
},
{
id: "product",
title: "Product",
order: 10,
width: 260,
minWidth: 220,
editor: "text",
validation: (zod: typeof z) =>
zod.string().trim().min(3, "Use at least 3 characters").max(80),
searchValue: (row) => `${row.product} ${row.brand} ${row.sku}`,
cell: ({ row }) => (
<div className="example-product-cell">
<img src={row.thumb} alt="" loading="lazy" />
<span>
<strong>{row.product}</strong>
<small>{row.brand} · {row.subcategory}</small>
</span>
</div>
),
exportValue: (row) => row.product,
},
{
id: "brand",
title: "Brand",
width: 100,
editor: "text",
validation: (zod: typeof z) => zod.string().trim().min(2).max(40),
},
{
id: "category",
title: "Category",
width: 120,
editor: "text",
validation: (zod: typeof z) => zod.string().trim().min(2).max(40),
},
{
id: "subcategory",
title: "Line",
width: 100,
editor: "text",
validation: (zod: typeof z) => zod.string().trim().min(2).max(40),
},
{
id: "region",
title: "Region",
order: 60,
width: 100,
editor: "text",
validation: (zod: typeof z) => zod.string().trim().min(2).max(20),
cell: ({ row, value }) => (
<span className="example-flag">
<img
src={`https://flagcdn.com/16x12/${row.countryCode}.png`}
width="16"
height="12"
alt=""
loading="lazy"
/>
{String(value ?? "")}
</span>
),
exportValue: (row) => row.region,
},
{
id: "warehouse",
title: "Warehouse",
width: 110,
editor: "text",
validation: (zod: typeof z) =>
zod.string().trim().regex(/^[A-Z]{3}-\d+$/, "Use a code like SFO-1"),
},
{
id: "stock",
title: "Stock",
order: 30,
width: 118,
align: "right",
filterType: "number",
editor: { type: "number", min: 0, max: 160, step: 1 },
validation: (zod: typeof z) =>
zod.coerce.number().int("Use whole units").min(0).max(160, "Stock cannot exceed 160"),
cell: ({ value }) => {
const n = Number(value);
const pct = Number.isFinite(n) ? Math.min(100, Math.max(0, (n / 160) * 100)) : 0;
return (
<span className="example-stock">
<span><i style={{ width: `${pct}%` }} /></span>
<strong>{Number.isFinite(n) ? n : ""}</strong>
</span>
);
},
exportValue: (row) => row.stock,
},
{
id: "reserved",
title: "Reserved",
width: 100,
align: "right",
filterType: "number",
editor: { type: "number", min: 0, max: 160, step: 1 },
validation: ({ rowValue, z: zod }: ValidationContext<ProductRow>) =>
zod.coerce.number().int().min(0).max(rowValue.stock, "Reserved cannot exceed stock"),
},
{
id: "reorderAt",
title: "Reorder",
width: 90,
align: "right",
filterType: "number",
editor: { type: "number", min: 0, max: 160, step: 1 },
validation: (zod: typeof z) => zod.coerce.number().int().min(0).max(160),
},
{
id: "price",
title: "Price",
width: 100,
align: "right",
filterType: "number",
editor: { type: "number", min: 1, max: 500, step: 0.01 },
validation: (zod: typeof z) =>
zod.coerce.number().min(1, "Price must be at least $1").max(500),
cell: ({ value }) => {
const n = Number(value);
return Number.isFinite(n) ? `$${n.toFixed(2)}` : "";
},
exportValue: (row) => row.price,
},
{
id: "cost",
title: "Cost",
width: 100,
align: "right",
filterType: "number",
editor: { type: "number", min: 0, max: 500, step: 0.01 },
validation: ({ rowValue, z: zod }: ValidationContext<ProductRow>) =>
zod.coerce.number().min(0).max(rowValue.price, "Cost cannot exceed price"),
cell: ({ value }) => {
const n = Number(value);
return Number.isFinite(n) ? `$${n.toFixed(2)}` : "";
},
exportValue: (row) => row.cost,
},
{
id: "margin",
title: "Margin %",
width: 100,
align: "right",
filterType: "number",
editor: { type: "number", min: 0, max: 100, step: 0.1 },
validation: (zod: typeof z) => zod.coerce.number().min(0).max(100),
},
{
id: "rating",
title: "Rating",
order: 40,
width: 112,
align: "center",
filterType: "number",
editor: { type: "number", min: 0, max: 5, step: 0.1 },
validation: (zod: typeof z) => zod.coerce.number().min(0).max(5),
cell: ({ value }) => {
const n = Number(value);
const pct = Number.isFinite(n) ? Math.min(100, Math.max(0, (n / 5) * 100)) : 0;
return (
<span className="example-rating" aria-label={`${Number.isFinite(n) ? n.toFixed(1) : "No"} rating`}>
<span
aria-hidden="true"
className="example-rating__stars"
style={{ ["--rating-pct" as string]: `${pct}%` }}
>
{"★︎".repeat(5)}
</span>
<strong>{Number.isFinite(n) ? n.toFixed(1) : ""}</strong>
</span>
);
},
exportValue: (row) => row.rating,
},
{
id: "reviews",
title: "Reviews",
width: 100,
align: "right",
filterType: "number",
editor: { type: "number", min: 0, max: 100_000, step: 1 },
validation: (zod: typeof z) => zod.coerce.number().int().min(0).max(100_000),
},
{
id: "status",
title: "Status",
order: 20,
width: 110,
editor: {
type: "select",
options: [
{ value: "Live", label: "Live" },
{ value: "Draft", label: "Draft" },
{ value: "Paused", label: "Paused" },
],
},
cell: ({ value }) => (
<span className={`example-status is-${String(value).toLowerCase()}`}>
{String(value ?? "")}
</span>
),
exportValue: (row) => row.status,
},
{
id: "owner",
title: "Owner",
width: 130,
editor: "text",
validation: (zod: typeof z) => zod.string().trim().min(2).max(60),
},
{
id: "featured",
title: "Hero",
order: 50,
width: 92,
align: "center",
headerAlign: "center",
editor: "checkbox",
cell: ({ value }) => (
<span className={`example-featured${value ? " is-on" : ""}`}>
{value ? "Featured" : "Standard"}
</span>
),
exportValue: (row) => (row.featured ? "true" : "false"),
},
{
id: "updatedAt",
title: "Updated",
width: 110,
editor: "date",
validation: (zod: typeof z) =>
zod.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Choose a valid date"),
},
];
export default function BasicExample({ theme }: ExampleProps) {
const [rows, setRows] = useState<ProductRow[]>(() =>
products.map((product) => ({ ...product }))
);
const tableColumns = useMemo(() => basicExampleColumns, []);
const disabledDemoRows = useMemo(
() => rows.filter((row) => row.sku === disabledDemoSku),
[rows]
);
return (
<DataTable
theme={theme}
title="Product catalog"
dataSource={rows}
onChange={setRows}
columnSettings={tableColumns}
disabledRows={disabledDemoRows}
maxHeight={480}
enablePagination
enableRowIndex
enableRowSelection
enableMultiRowSelection
pageSizeOptions={[25, 50, 100]}
enableColumnResizing
columnResizeMode="live"
enableColumnReorder
enableColumnSettings
enableEditing
enableDownload
enableUpload
enableRowActions
defaultFileName="product-catalog"
xlsxAdapter={basicExampleXlsxAdapter}
undoLimit={20}
redoLimit={20}
/>
);
}
Related API