React Grid / Documentation

A great table,
in a few lines.

Install the package, bring your data, and make it yours. React 18.2 or later, with TypeScript support built in.

Installation

Add the package and Tailwind CSS v4 to your React app. React and React DOM are peer dependencies; other runtime dependencies install automatically.

npm install @dynostack/react-grid\nnpm install -D tailwindcss @tailwindcss/vite

In your Vite configuration, register the Tailwind plugin:

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ plugins: [tailwindcss()] })

Import the grid styles and scan its distributed JavaScript from your global stylesheet. Adjust the relative source path for your CSS file.

@import "tailwindcss";
@source "../node_modules/@dynostack/react-grid/dist";
@import "@dynostack/react-grid/styles.css";

Using Tailwind v3? See the v3 setup in the README. Next.js components that render the grid should use "use client".

Quick start

Every row needs a unique, stable id. Keep your column definitions stable. Your callbacks own persistence; editing a cell does not send a request by itself.

import { useState } from 'react'
import { DataTable, type DataTableColumn } from '@dynostack/react-grid'

type Project = { id: string; name: string; budget: number }
const columns: DataTableColumn<Project>[] = [
  { accessorKey: 'name', header: 'Project',
    meta: { editor: 'text', isEditable: true } },
  { accessorKey: 'budget', header: 'Budget',
    meta: { editor: 'currency', filterType: 'number' } },
]

export default function Projects() {
  const [rows, setRows] = useState<Project[]>([
    { id: '1', name: 'Website redesign', budget: 12000 },
  ])
  return <DataTable data={rows} columns={columns}
    rowActions={['view', 'edit', 'delete']}
    onCellEdit={(row, key, value) => setRows(previous =>
      previous.map(item => item.id === row.id
        ? { ...item, [key]: value } : item))}
    onRowSave={(row, draft) => setRows(previous =>
      previous.some(item => item.id === row.id)
        ? previous.map(item => item.id === row.id ? { ...item, ...draft } : item)
        : [...previous, { ...row, ...draft }])}
    onDelete={(row) => setRows(previous => previous.filter(item => item.id !== row.id))}
    onAddRow={() => ({ id: crypto.randomUUID(), name: '', budget: 0 })}
  />
}

Columns & editing

Use TanStack column definitions with meta to configure labels, editors, filters, alignment, and export visibility.

{ accessorKey: 'status', header: 'Status',
  meta: {
    label: 'Status', editor: 'select', filterType: 'multi-select',
    selectOptions: [{ value: 'Done', label: 'Done' }],
    badgeMap: { Done: 'success' },
    isEditable: (row) => row.status !== 'Archived',
    exportable: true, align: 'left',
  }
}

Editors: text, number, currency, date, select, switch, checkbox. Filters: text, number, date, select, multi-select, boolean. Use onCellEdit for single edits and onRowSave for row drafts. Read-only flags apply to row edit mode too. Validate permissions and values again on your server.

Features

All capabilities are opt-in configurable. Set any flag below to false. Refresh and add-row buttons also require their callbacks.

<DataTable data={rows} columns={columns}
  features={{ search: true, refresh: true, columnVisibility: true,
    export: true, addRow: true, pagination: true, sorting: true,
    filtering: true, resizing: true, reordering: true, pinning: true }}
  onRefresh={reloadRows}
  onAddRow={() => ({ id: crypto.randomUUID() })}
/>

Pagination off displays all matching loaded rows. In server mode, only fetched rows are available. Use labels to translate toolbar and empty-state text; see the README for the supported label keys.

Themes & layout

import { themePresets, buildPreset } from '@dynostack/react-grid'

<DataTable data={rows} columns={columns}
  theme={themePresets.graphite} isolate
  density="comfortable" striped stickyHeader maxHeight="480px"
  initialSorting={[{ id: 'name', desc: false }]}
  initialColumnPinning={{ left: ['name'], right: [] }}
  initialColumnVisibility={{ budget: false }}
  ariaLabel="Project workspace"
/>
// Custom color: theme={buildPreset(180)}

Presets: graphite, neutral, light, dark, violet, emerald, amber, rose, sky, slate. Moded themes follow the OS or a .dark ancestor. For a forced light or dark grid, pass theme={{ light: preset.light }} or theme={{ light: preset.dark }}. Density supports compact, default, and comfortable.

Selection & expansion

<DataTable data={rows} columns={columns}
  enableSelection
  onSelectionChange={setSelectedRows}
  onBulkDelete={(selected) => deleteProjects(selected.map(row => row.id))}
  renderSubRow={(row) => <ProjectDetails project={row} />}
  rowActions={['view', 'edit', 'duplicate', 'delete']}
  onRowAction={(action, row) => {
    if (action === 'duplicate') duplicateProject(row)
  }}
/>

Deletion asks for confirmation by default. View opens a details sheet; use viewSheet to customize it. Use getSubRows for nested rows. In server mode, selection callbacks and exports contain loaded rows only.

Data fetching

Pass controlled data or a stable dataSource. Client mode fetches all rows, then sorts and filters locally. Server mode passes the current query to your API.

const dataSource = useMemo(() => ({
  mode: 'server' as const,
  fetchRows: async ({ pageIndex, pageSize, sorting, columnFilters, globalFilter }) => {
    const response = await fetch('/api/projects/query', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ pageIndex, pageSize, sorting, columnFilters, globalFilter }),
    })
    if (!response.ok) throw new Error('Could not load projects')
    return response.json() // { rows: Project[], totalRecords: number }
  },
}), [])
<DataTable columns={columns} dataSource={dataSource} />

The API must validate page sizes, allowlist sortable and filterable columns, and enforce authorization. Avoid putting user-provided column identifiers into raw SQL.

Export & security

Exports include visible, exportable columns and selected rows, or all filtered loaded rows if nothing is selected. CSV escaping protects quotes and line breaks; spreadsheet formula prefixes in text are neutralized. Real numeric values stay numeric. Excel export is HTML-based .xls, not native XLSX; Excel may show a format warning.

Set meta.exportable = false for fields that should not download. Hidden fields remain in JavaScript, so keep secrets and unauthorized records out of the browser entirely. React escapes row text. Theme tokens accept CSS values and reject declaration breakouts or URL expressions.

API reference

PropPurpose
data / columnsRows and TanStack column definitions
dataSourceInternal client or server fetcher
features / labelsFeature switches and supported text overrides
theme / isolate / densityPer-instance appearance
onCellEdit / onRowSavePersist edits
onAddRow / onDelete / onBulkDeleteCreate and remove records
rowActions / customRowActionsBuilt-in and custom row menus
initialSortingStarting sort order
onSelectionChangeSelected loaded row objects
striped / stickyHeader / maxHeightRow styling and scroll viewport
ariaLabelAccessible table name

See the complete typed API reference for view sheets, delete confirmation, pagination, filters, and exported helpers.

Downloads

Install the published package from npm or download a minimal example and playground configuration.

The playground runs the source package in this repository. Install the latest npm version to use the same layout controls and export protections.