npm.io
1.2.1 • Published 2d ago

@sukritt/datagrid

Licence
MIT
Version
1.2.1
Deps
1
Size
2.4 MB
Vulns
0
Weekly
0

@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 (flex distributes 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 locale prop + per-string overrides with localeText
  • 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
  • columnDefs accepts ColumnDef and ColumnGroupDef mixed 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

Keywords