react-datatable
Examples

Local Data Table

This example shows how common features work together in one local-mode table. You you can copy, run, and then adapt to your own product. It builds a local table with typed rows, stable row IDs, search, filters, display options, selection, bulk actions, keyboard navigation, and preview using static in-file data with no custom cells.

Loading table preferences...

Before you start, install the datatable package and set it up. See Installation.

1. Row type and data

Define one Customer type and keep the data array in the same file so the example is fully copyable.

// client
type Customer = {
  id: string
  name: string
  company: string
  status: "Active" | "Trial" | "Paused"
  plan: "Starter" | "Team" | "Enterprise"
  seats: number
  revenue: number
  owner: string
  region: "NA" | "EU" | "APAC"
  renewal: string
}

const rows: Customer[] = [
  { id: "cus_0001", name: "Ava Patel", company: "Northstar Labs", status: "Active", plan: "Enterprise", seats: 185, revenue: 42100, owner: "Jordan Lee", region: "NA", renewal: "2026-09-14" },
  { id: "cus_0002", name: "Noah Kim", company: "Blue Harbor Health", status: "Trial", plan: "Team", seats: 42, revenue: 7600, owner: "Marta Rossi", region: "EU", renewal: "2026-06-02" },
  { id: "cus_0003", name: "Emma Silva", company: "Orbit Retail Group", status: "Active", plan: "Team", seats: 96, revenue: 18300, owner: "Jordan Lee", region: "NA", renewal: "2026-11-08" },
  // ...16 additional rows with the same shape
]

This is a good local-mode shape because the browser owns the full dataset and the table can evaluate search, filters, sorting, grouping, and selection without a server query layer.

2. Define columns

The example columns do more than render labels. They decide filtering, sorting, grouping, and widths with built-in behaviors only.

// client
const columns: DatatableColumn<Customer>[] = [
  {
    id: "company",
    header: "Company",
    accessorKey: "company",
    width: 250,
    enableSorting: true,
    enableFiltering: true,
    filterType: "text",
  },
  {
    id: "name",
    header: "Contact",
    accessorKey: "name",
    width: 190,
    enableSorting: true,
    enableFiltering: true,
    filterType: "text",
  },
  {
    id: "status",
    header: "Status",
    accessorKey: "status",
    width: 140,
    enableSorting: true,
    enableFiltering: true,
    enableGrouping: true,
    filterType: "text-list",
    filterOptions: {
      options: statuses.map((status) => ({ label: status, value: status })),
    },
  },
  {
    id: "revenue",
    header: "Revenue",
    accessorKey: "revenue",
    width: 130,
    enableSorting: true,
    enableFiltering: true,
    filterType: "number",
    cell: ({ getValue }) => `$${Number(getValue()).toLocaleString()}`,
  },
]

The full example also includes plan, seats, owner, region, and renewal to demonstrate text, list, number, and date filters in one place.

3. Mount with stable row IDs

The core table stays simple:

// client
<Datatable
  tableKey="customers"
  data={rows}
  columns={columns}
  getRowId={(row) => row.id}
  initialState={{
    sorting: [{ id: "name", desc: false }],
    showHorizontalLines: false,
    showVerticalLines: false,
  }}
/>

That gives the example a stable contract before any extra interaction layers are added.

4. Toolbar

The runnable example turns on quick search, filters, and display options from the toolbar.

// client
toolbar={{
  quickSearch: {
    placeholder: "Search 1,200 customers...",
  },
  filterButton: true,
  displayOptions: true,
  copyLink: false,
  views: false,
}}

This is a good local example mix because it focuses on browsing the current dataset, not URL sharing or saved views.

5. Selection and bulk actions

Selection is enabled together with a small bulk-actions registry.

// client
selection={{
  enabled: true,
  mode: "multi",
  showCheckboxOnHover: false,
  allowSelectAllMatching: true,
}}
bulkActions={{
  triggerLabel: "Actions",
  actions: bulkActions,
}}

The example bulk actions intentionally do lightweight local work:

  • export selected IDs
  • mark selected customers as reviewed

Each action also clears selection and closes the dialog so the workflow feels complete after the operation finishes.

6. Keyboard navigation and preview

The example supports both keyboard-first navigation and a floating preview panel.

// client
keyboardNavigation={{
  enabled: true,
}}
rowActions={{
  onOpenRow: ({ row }) => {
    setLastAction(`Opened ${row.company}.`)
  },
  onTogglePreviewRow: ({ row, nextOpen }) => {
    setLastAction(`${nextOpen ? "Previewing" : "Closed preview for"} ${row.company}.`)
  },
}}
preview={{
  floating: {
    draggable: true,
    storageKey: "react-datatable-local-basic",
  },
  renderPreview: ({ row }) => (
    <CustomerPreview customer={row} />
  ),
}}

This is a good example of the local table acting like a real product surface instead of a bare data grid.

Full example (single file)

Expand to copy the full local customer table example
// client
import { useMemo, useState } from "react"
import { Datatable, type DataTableBulkAction, type DatatableColumn } from "./react-datatable"

type CustomerStatus = "Active" | "Trial" | "Paused"
type CustomerPlan = "Starter" | "Team" | "Enterprise"
type CustomerRegion = "NA" | "EU" | "APAC"

type Customer = {
  id: string
  name: string
  company: string
  status: CustomerStatus
  plan: CustomerPlan
  seats: number
  revenue: number
  owner: string
  region: CustomerRegion
  renewal: string
}

const rows: Customer[] = [
  { id: "cus_0001", name: "Ava Patel", company: "Northstar Labs", status: "Active", plan: "Enterprise", seats: 185, revenue: 42100, owner: "Jordan Lee", region: "NA", renewal: "2026-09-14" },
  { id: "cus_0002", name: "Noah Kim", company: "Blue Harbor Health", status: "Trial", plan: "Team", seats: 42, revenue: 7600, owner: "Marta Rossi", region: "EU", renewal: "2026-06-02" },
  { id: "cus_0003", name: "Emma Silva", company: "Orbit Retail Group", status: "Active", plan: "Team", seats: 96, revenue: 18300, owner: "Jordan Lee", region: "NA", renewal: "2026-11-08" },
  { id: "cus_0004", name: "Liam Turner", company: "Cinder Logistics", status: "Paused", plan: "Starter", seats: 18, revenue: 2400, owner: "Priya Nair", region: "APAC", renewal: "2026-05-29" },
  { id: "cus_0005", name: "Mia Dubois", company: "Kepler Finance", status: "Active", plan: "Enterprise", seats: 220, revenue: 50900, owner: "Marta Rossi", region: "EU", renewal: "2026-12-01" },
  { id: "cus_0006", name: "Ethan Brooks", company: "Summit Bio", status: "Trial", plan: "Starter", seats: 24, revenue: 3200, owner: "Diego Alvarez", region: "NA", renewal: "2026-07-10" },
  { id: "cus_0007", name: "Sophia Chen", company: "Copperline Energy", status: "Active", plan: "Team", seats: 88, revenue: 15400, owner: "Jordan Lee", region: "EU", renewal: "2026-10-21" },
  { id: "cus_0008", name: "Lucas Meyer", company: "Atlas Devices", status: "Paused", plan: "Team", seats: 57, revenue: 9100, owner: "Priya Nair", region: "NA", renewal: "2026-08-03" },
  { id: "cus_0009", name: "Isabella Reed", company: "Helio Security", status: "Active", plan: "Enterprise", seats: 164, revenue: 33700, owner: "Diego Alvarez", region: "APAC", renewal: "2026-09-30" },
  { id: "cus_0010", name: "James Park", company: "Maple Education", status: "Trial", plan: "Team", seats: 39, revenue: 6900, owner: "Marta Rossi", region: "EU", renewal: "2026-06-27" },
  { id: "cus_0011", name: "Charlotte Gray", company: "Riverbend Media", status: "Active", plan: "Starter", seats: 31, revenue: 5100, owner: "Priya Nair", region: "NA", renewal: "2026-10-05" },
  { id: "cus_0012", name: "Benjamin Cole", company: "Stonegate Manufacturing", status: "Paused", plan: "Enterprise", seats: 142, revenue: 27100, owner: "Jordan Lee", region: "EU", renewal: "2026-07-19" },
  { id: "cus_0013", name: "Amelia Ward", company: "Vista Hospitality", status: "Active", plan: "Team", seats: 73, revenue: 12900, owner: "Diego Alvarez", region: "APAC", renewal: "2026-11-15" },
  { id: "cus_0014", name: "Henry Price", company: "Signal Works", status: "Trial", plan: "Starter", seats: 16, revenue: 2100, owner: "Marta Rossi", region: "NA", renewal: "2026-05-18" },
  { id: "cus_0015", name: "Harper Quinn", company: "Lumen Foods", status: "Active", plan: "Enterprise", seats: 198, revenue: 44800, owner: "Jordan Lee", region: "EU", renewal: "2026-12-12" },
  { id: "cus_0016", name: "Alexander Diaz", company: "Nimbus Telecom", status: "Paused", plan: "Team", seats: 64, revenue: 11200, owner: "Priya Nair", region: "APAC", renewal: "2026-08-24" },
  { id: "cus_0017", name: "Evelyn Scott", company: "Forge Mobility", status: "Active", plan: "Team", seats: 104, revenue: 20100, owner: "Diego Alvarez", region: "NA", renewal: "2026-09-07" },
  { id: "cus_0018", name: "Michael Evans", company: "Prism Insurance", status: "Trial", plan: "Starter", seats: 22, revenue: 2800, owner: "Marta Rossi", region: "EU", renewal: "2026-06-14" },
  { id: "cus_0019", name: "Abigail Hill", company: "Tidal Commerce", status: "Active", plan: "Enterprise", seats: 176, revenue: 39200, owner: "Jordan Lee", region: "NA", renewal: "2026-11-28" },
  { id: "cus_0020", name: "Daniel Young", company: "Pioneer Clinics", status: "Paused", plan: "Starter", seats: 28, revenue: 3600, owner: "Priya Nair", region: "APAC", renewal: "2026-07-01" },
]

const statuses: CustomerStatus[] = ["Active", "Trial", "Paused"]
const plans: CustomerPlan[] = ["Starter", "Team", "Enterprise"]
const regions: CustomerRegion[] = ["NA", "EU", "APAC"]

function CustomerPreview({ customer }: { customer: Customer }) {
  return (
    <article className="mx-auto max-w-lg text-sm text-foreground">
      <h3 className="m-0 text-xl font-semibold">{customer.company}</h3>
      <p className="m-0 mt-1 text-muted-foreground">{customer.name}</p>
      <dl className="mt-4 grid grid-cols-2 gap-x-6 gap-y-2">
        <dt className="text-muted-foreground">Status</dt>
        <dd className="m-0 text-right">{customer.status}</dd>
        <dt className="text-muted-foreground">Plan</dt>
        <dd className="m-0 text-right">{customer.plan}</dd>
        <dt className="text-muted-foreground">Owner</dt>
        <dd className="m-0 text-right">{customer.owner}</dd>
        <dt className="text-muted-foreground">Region</dt>
        <dd className="m-0 text-right">{customer.region}</dd>
        <dt className="text-muted-foreground">Seats</dt>
        <dd className="m-0 text-right">{customer.seats}</dd>
        <dt className="text-muted-foreground">Revenue</dt>
        <dd className="m-0 text-right">${customer.revenue.toLocaleString()}</dd>
        <dt className="text-muted-foreground">Renewal</dt>
        <dd className="m-0 text-right">{customer.renewal}</dd>
      </dl>
    </article>
  )
}

export function LocalCustomerTableExample() {
  const [lastAction, setLastAction] = useState("Ready: local rows loaded.")

  const columns = useMemo<DatatableColumn<Customer>[]>(() => [
    {
      id: "company",
      header: "Company",
      accessorKey: "company",
      width: 250,
      enableSorting: true,
      enableFiltering: true,
      filterType: "text",
    },
    {
      id: "name",
      header: "Contact",
      accessorKey: "name",
      width: 190,
      enableSorting: true,
      enableFiltering: true,
      filterType: "text",
    },
    {
      id: "status",
      header: "Status",
      accessorKey: "status",
      width: 130,
      enableSorting: true,
      enableFiltering: true,
      enableGrouping: true,
      filterType: "text-list",
      filterOptions: {
        options: statuses.map((status) => ({ label: status, value: status })),
      },
    },
    {
      id: "plan",
      header: "Plan",
      accessorKey: "plan",
      width: 130,
      enableSorting: true,
      enableFiltering: true,
      enableGrouping: true,
      filterType: "text-list",
      filterOptions: {
        options: plans.map((plan) => ({ label: plan, value: plan })),
      },
    },
    {
      id: "region",
      header: "Region",
      accessorKey: "region",
      width: 120,
      enableSorting: true,
      enableFiltering: true,
      enableGrouping: true,
      filterType: "text-list",
      filterOptions: {
        options: regions.map((region) => ({ label: region, value: region })),
      },
    },
    {
      id: "seats",
      header: "Seats",
      accessorKey: "seats",
      width: 110,
      enableSorting: true,
      enableFiltering: true,
      filterType: "number",
    },
    {
      id: "revenue",
      header: "Revenue",
      accessorKey: "revenue",
      width: 130,
      enableSorting: true,
      enableFiltering: true,
      filterType: "number",
      cell: ({ getValue }) => `$${Number(getValue()).toLocaleString()}`,
    },
    {
      id: "owner",
      header: "Owner",
      accessorKey: "owner",
      width: 170,
      enableSorting: true,
      enableFiltering: true,
      enableGrouping: true,
      filterType: "text-list",
      filterOptions: {
        options: Array.from(new Set(rows.map((row) => row.owner))).map((owner) => ({ label: owner, value: owner })),
      },
    },
    {
      id: "renewal",
      header: "Renewal",
      accessorKey: "renewal",
      width: 140,
      enableSorting: true,
      enableFiltering: true,
      filterType: "date",
    },
  ], [])

  const bulkActions = useMemo<DataTableBulkAction<Customer>[]>(() => [
    {
      id: "mark-reviewed",
      title: "Mark as reviewed",
      keywords: ["review", "done"],
      onSelect: (context) => {
        setLastAction(`Marked ${context.selectedCount} customer${context.selectedCount === 1 ? "" : "s"} as reviewed.`)
        context.clearSelection()
        context.closeDialog()
      },
    },
    {
      id: "export-selection",
      title: "Export selected IDs",
      keywords: ["export", "ids"],
      onSelect: (context) => {
        const ids = context.selectedRows.map((row) => row.id).join(", ")
        setLastAction(ids.length > 0 ? `Selected IDs: ${ids}` : "No rows selected.")
        context.clearSelection()
        context.closeDialog()
      },
    },
  ], [])

  return (
    <section className="rounded-xl border border-border bg-card p-4">
      <p className="mb-3 text-sm text-muted-foreground">{lastAction}</p>
      <div className="h-[560px] overflow-hidden rounded-lg border border-border">
        <Datatable
          tableKey="customers"
          data={rows}
          columns={columns}
          getRowId={(row) => row.id}
          toolbar={{
            quickSearch: {
              placeholder: "Search 20 customers...",
            },
            filterButton: true,
            displayOptions: true,
            copyLink: false,
            views: false,
          }}
          initialState={{
            sorting: [{ id: "company", desc: false }],
            showHorizontalLines: false,
            showVerticalLines: false,
          }}
          selection={{
            enabled: true,
            mode: "multi",
            showCheckboxOnHover: false,
            allowSelectAllMatching: true,
          }}
          bulkActions={{
            triggerLabel: "Actions",
            actions: bulkActions,
          }}
          keyboardNavigation={{
            enabled: true,
          }}
          rowActions={{
            onOpenRow: ({ row }) => {
              setLastAction(`Opened ${row.company}.`)
            },
            onTogglePreviewRow: ({ row, nextOpen }) => {
              setLastAction(`${nextOpen ? "Previewing" : "Closed preview for"} ${row.company}.`)
            },
          }}
          preview={{
            floating: {
              draggable: true,
              storageKey: "docs-local-customer-example",
            },
            renderPreview: ({ row }) => <CustomerPreview customer={row} />,
          }}
        />
      </div>
    </section>
  )
}

Where to go next

On this page