@sukritt/datagrid
@sukritt/datagrid
English · ไทย
Custom zero-dependency DataGrid component for React with virtual scroll, cell editing, filtering, sorting, grouping, and export support.
Features
- Virtual scroll for large datasets
- Pagination
- Sort (click a header — Shift+click for multi-column sort), Filter (text / number / date / select dropdown)
- Global search + a "clear all filters" button
- Keyboard navigation — arrow keys / Home / End / PageUp / PageDown / Enter to edit / Space to select a row
- Column Group Headers — group columns under a parent header (multi-level)
- Column resize & drag-and-drop reorder (group width updates automatically)
- Freeze columns left and right (right-click a header or cell)
- Flex column sizing (
flexdistributes remaining space by ratio) - Cell editing — 4 built-in editors (
text/number/date/select) + custom editor - Row selection (single / multiple with checkbox + select-all)
- Group by (drag a header onto the group bar)
- Column visibility toggle
- Aggregation (sum / avg / count / min / max) + a pinned grand-total row
- Master-detail — a detail panel under each row via
renderDetail - Clipboard — Ctrl+C to copy a cell, Ctrl+V to paste multiple cells from Excel/Sheets
- Column virtualization — kicks in automatically past 30 columns (only visible columns are rendered)
- Per-column custom filter via
filterFn - CSV & Excel export
- Server-side mode — sort / filter / pagination handled by your backend
- CSS variable theming via
--dg-*custom properties + a ready-made dark theme (dg-theme-dark) - Density toggle — compact / normal / comfortable from the toolbar
- i18n — Thai (default) / English UI via the
localeprop + per-string overrides withlocaleText - TypeScript ready
Installation
npm install @sukritt/datagrid
Usage
import { DataGrid } from '@sukritt/datagrid'
import '@sukritt/datagrid/styles'
import type { ColumnDef, DataGridRef } from '@sukritt/datagrid'
interface Product {
id: number
name: string
price: number
}
const columns: ColumnDef<Product>[] = [
{ field: 'id', headerName: 'ID', width: 80 },
{ field: 'name', headerName: 'Name', flex: 1 },
{ field: 'price', headerName: 'Price', width: 120, align: 'right', filter: 'number' },
]
export default function App() {
return (
<DataGrid<Product>
rowData={data}
columnDefs={columns}
height={400}
enableFilter
enableCellEdit
rowSelection="multiple"
pagination={{ enabled: true, pageSize: 20 }}
/>
)
}
Export with ref
import { useRef } from 'react'
import { DataGrid } from '@sukritt/datagrid'
import type { DataGridRef } from '@sukritt/datagrid'
const gridRef = useRef<DataGridRef>(null)
<DataGrid ref={gridRef} rowData={data} columnDefs={columns} />
<button onClick={() => gridRef.current?.exportToCsv('report')}>Export CSV</button>
<button onClick={() => gridRef.current?.exportToExcel('report')}>Export Excel</button>
Column Group Headers
Group columns under a shared parent header using ColumnGroupDef in columnDefs.
How it works: columns that should sit under the same group go in the ColumnGroupDef's children array — columns outside children render as normal columns.
columnDefs = [
ColumnDef <- regular column (no group header)
ColumnDef <- regular column
ColumnGroupDef <- group header "AMOUNT"
+-- ColumnDef <- under "AMOUNT"
+-- ColumnDef <- under "AMOUNT"
+-- ColumnDef <- under "AMOUNT"
]
import type { ColumnDefOrGroup } from '@sukritt/datagrid'
interface SalesRow {
customer: string
region: string
amount2025: number
amount2026: number
lifetime: number
}
const columns: ColumnDefOrGroup<SalesRow>[] = [
// regular columns -- not part of any group
{ field: 'customer', headerName: 'Customer', width: 200 },
{ field: 'region', headerName: 'Region', width: 120 },
// ColumnGroupDef -- columns in children render under "AMOUNT"
{
groupId: 'amount',
headerName: 'AMOUNT',
headerAlign: 'center',
children: [
{ field: 'amount2025', headerName: '2025 Total', width: 160, align: 'right' },
{ field: 'amount2026', headerName: '2026 Total', width: 160, align: 'right' },
{ field: 'lifetime', headerName: 'Lifetime Total', width: 180, align: 'right' },
],
},
]
export default function App() {
return <DataGrid rowData={data} columnDefs={columns} height={500} />
}
Note: group header names and their member columns can also be managed from the Sidebar (the columns button, top-right) without touching code.
Managing Column Groups from the Sidebar
Click the columns button in the top-right of the grid to open the Sidebar, then go to the Column Groups section.
| Action | How |
|---|---|
| Create a new group | Click +, type a name, press Enter or the blue + button |
| Add a column | Drag a column from the list above into the group's drop zone |
| Remove a column | Click x next to the column name inside the group |
| Rename a group | Click the group name and type a new one |
| Delete a group | Click x next to the group header (columns return to normal) |
ColumnGroupDef Options
| Option | Type | Description |
|---|---|---|
groupId |
string |
Unique group ID (required) |
headerName |
string |
Group header label (required) |
children |
ColumnDef<TData>[] |
Columns that belong to this group (required) |
headerAlign |
'left' | 'center' | 'right' |
Label alignment (default: center) |
headerStyle |
CSSProperties |
Custom style for the group header cell |
columnDefsacceptsColumnDefandColumnGroupDefmixed together- Group header width is computed from its child columns automatically, and updates on resize/hide
- A grid with no groups renders a single header row as before (backward compatible)
Props
| Prop | Type | Default | Description |
|---|---|---|---|
rowData |
TData[] |
required | Data shown in the grid |
columnDefs |
ColumnDefOrGroup<TData>[] |
required | Column definitions; accepts ColumnGroupDef too |
height |
number | string |
500 | Grid height |
rowHeight |
number |
40 | Row height (px) |
headerHeight |
number |
40 | Header height (px) |
density |
'compact' | 'normal' | 'comfortable' |
normal | Initial density (togglable from the toolbar) |
locale |
'th' | 'en' |
th | UI language |
localeText |
Partial<DataGridLocale> |
- | Per-string UI text overrides |
pagination |
PaginationOptions |
enabled, 20/page | Pagination settings |
enableFilter |
boolean |
true | Enable the filter row |
enableColumnResize |
boolean |
true | Enable drag-resize on columns |
enableColumnReorder |
boolean |
true | Enable drag-reorder on columns |
enableCellEdit |
boolean |
false | Double-click to edit a cell |
rowSelection |
'single' | 'multiple' | false |
false | Row selection mode |
showTotalsRow |
boolean |
true | Pinned grand-total row (shown once Values are set in the sidebar) |
renderDetail |
(params) => ReactNode |
- | Detail panel under a row (master-detail) |
detailRowHeight |
number |
240 | Detail panel height (px, fixed) |
loading |
boolean |
false | Show the loading overlay |
noRowsText |
string |
"ไม่มีข้อมูล" | Text shown when there's no data |
onRowClick |
(data, rowIndex) => void |
- | Callback when a row is clicked |
onCellValueChanged |
(params) => void |
- | Callback when a cell is edited |
onSelectionChanged |
(selectedRows) => void |
- | Callback when selection changes |
serverSide |
boolean |
false | Enable server-side mode (sort/filter/pagination handled by the backend) |
totalRows |
number |
- | Total row count from the server (used with serverSide) |
onServerRequest |
(params: ServerRequestParams) => void |
- | Fired when page/sort/filter changes in server mode |
style |
CSSProperties |
- | Inline style — also used to override CSS variables ({ '--dg-primary': '#f00' }) |
Server-side mode
When serverSide is true, the grid stops processing rowData itself and instead fires onServerRequest whenever page / sort / filter changes. The consumer fetches data and feeds back rowData (current page only) + totalRows + loading (a controlled pattern).
const [rows, setRows] = useState<Row[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(false)
const handleRequest = (p: ServerRequestParams) => {
setLoading(true)
fetch(`/api/rows?page=${p.page}&size=${p.pageSize}` +
`&sort=${p.sortModel?.field ?? ''}:${p.sortModel?.direction ?? ''}` +
`&q=${p.filterModel.globalSearch}`)
.then(r => r.json())
.then(({ rows, total }) => { setRows(rows); setTotal(total); setLoading(false) })
}
<DataGrid
serverSide
rowData={rows}
totalRows={total}
loading={loading}
onServerRequest={handleRequest}
columnDefs={cols}
/>
ServerRequestParams = { page (1-based), pageSize, sortModel: SortModel | null, sortModels: SortModel[], filterModel: FilterModel } — sortModels is the full sort set in priority order (multi-sort via Shift+click); sortModel is the first entry, kept for backward compatibility. Filter/search is debounced 300ms; when sort/filter changes, the grid automatically resets to page 1 and fires a single request.
Note: grouping isn't supported in server mode (yet); select-filter options are derived from the current page only.
Theming (CSS variables)
The grid uses CSS custom properties (--dg-*) declared on :root, all with sensible defaults — override them at any level:
/* app-wide override */
:root { --dg-primary: #e11d48; --dg-row-selected: #fce7f3; }
/* per-instance override via the style prop */
<DataGrid style={{ '--dg-primary': '#e11d48' } as CSSProperties} ... />
Key tokens: --dg-primary, --dg-primary-dark, --dg-primary-light, --dg-surface, --dg-border-color, --dg-text, --dg-text-muted, --dg-text-secondary, --dg-danger, --dg-header-bg, --dg-header-hover, --dg-row-bg, --dg-row-alt, --dg-row-hover, --dg-row-selected, --dg-btn-border, --dg-icon-muted, --dg-on-primary, --dg-editable-hover, --dg-overlay-bg, --dg-font, --dg-font-size(-sm/-xs), --dg-cell-padding, --dg-radius(-sm), --dg-shadow(-md/-lg), --dg-transition(-fast)
Ready-made dark theme
Add the dg-theme-dark class to <html> or <body> (recommended — portal-rendered menus/dropdowns pick up the theme too):
document.documentElement.classList.toggle('dg-theme-dark', isDark)
Language (i18n)
<DataGrid locale="en" /> {/* full English UI */}
<DataGrid locale="th" localeText={{ noRows: 'ว่างเปล่า' }} /> {/* per-string override */}
LOCALE_TH / LOCALE_EN and the DataGridLocale type are exported from the package for building your own locale.
ColumnDef
| Option | Type | Description |
|---|---|---|
field |
string |
Key in rowData (required) |
headerName |
string |
Header label |
width |
number |
Fixed width (px) |
flex |
number |
Distributes remaining space by ratio (respects minWidth/maxWidth; a user drag-resize takes over from flex) |
align |
'left' | 'center' | 'right' |
Alignment |
filter |
boolean | 'text' | 'number' | 'date' | 'select' |
Filter type |
filterFn |
(cellValue, filterValue, data) => boolean |
Custom filter — replaces this column's built-in filter logic |
sortable |
boolean |
Whether the column can be sorted |
resizable |
boolean |
Whether the column can be resized |
editable |
boolean |
Whether the cell can be edited |
cellEditor |
'text' | 'number' | 'date' | 'select' | (params) => ReactNode |
Editor type (default text) — number commits a real number, select renders a dropdown, a function is a custom editor |
cellEditorOptions |
string[] | (params) => string[] |
Options for the select editor (defaults to unique values from the data) |
hide |
boolean |
Hide the column |
cellRenderer |
(params) => ReactNode |
Custom cell renderer |
valueFormatter |
(params) => string |
Format the value before display |
cellStyle |
CSSProperties | (params) => CSSProperties |
Cell style |
DataGridRef Methods
| Method | Description |
|---|---|
exportToCsv(fileName?) |
Download as CSV |
exportToExcel(fileName?) |
Download as Excel (.xlsx) |
getSelectedRows() |
Get an array of selected rows |
selectAll() |
Select all rows |
deselectAll() |
Clear the selection |
clearFilters() |
Clear every column filter + global search |
autoSizeColumns() |
Auto-size all columns to fit content |
autoSizeColumn(field) |
Auto-size a single column |
getState() |
Snapshot the full layout (DataGridState — JSON-serializable) |
setState(state) |
Restore layout from a snapshot (only the fields you pass are applied) |
State persistence (saving a user's layout)
getState() returns a snapshot of everything a user can adjust — column order/widths/hidden/frozen state, sort, filter, page, density, group-by, aggregation, and column group overrides:
// save
localStorage.setItem('my-grid', JSON.stringify(gridRef.current!.getState()))
// restore (e.g. on mount)
const saved = localStorage.getItem('my-grid')
if (saved) gridRef.current!.setState(JSON.parse(saved))
Peer Dependencies
{
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
License
MIT