react-gridix 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # react-gridix
2
2
 
3
- A React `DataGrid` with per-column filters, in-cell editing, nested fields, themes and custom components, plus the `Checkbox` it uses.
3
+ A React `DataGrid` with row selection, server-side mode, typed per-column filters, date columns, in-cell editing, resizing and pinning, virtualization, CSV export and 8 themes, plus the `Checkbox` it uses.
4
4
 
5
5
  **Contents**
6
6
 
@@ -13,6 +13,7 @@ A React `DataGrid` with per-column filters, in-cell editing, nested fields, them
13
13
  7. [Demos](#7-demos)
14
14
  8. [Development, building and publishing](#8-development-building-and-publishing)
15
15
  9. [Troubleshooting](#9-troubleshooting)
16
+ 10. [Migrating from the earlier version](#10-migrating-from-the-earlier-version)
16
17
 
17
18
  ---
18
19
 
@@ -20,10 +21,29 @@ A React `DataGrid` with per-column filters, in-cell editing, nested fields, them
20
21
 
21
22
  | Component | Purpose |
22
23
  | --- | --- |
23
- | `DataGrid` | Data table with sorting, search, per-column filters, pagination, in-cell editing, nested fields, striping, themes and custom column components |
24
- | `Checkbox` | Checkbox in three looks (button with a span, tick, native), toggles with one click. The grid uses it for boolean columns, and you can use it on its own |
24
+ | `DataGrid` | Data table with per-column sorting, search, typed per-column filters, pagination, in-cell editing, date columns, row selection, server-side mode, loading skeletons, resize / pin / hide, keyboard navigation, CSV export, virtualization, nested fields, striping, themes and custom column components |
25
+ | `Checkbox` | Checkbox in three looks (button with a span, tick, native), toggles with one click. The grid uses it for boolean columns and row selection, and you can use it on its own |
25
26
 
26
- Everything is written in TypeScript, ships its own type definitions, and uses plain CSS (no CSS-in-JS runtime). The grid is 100% of its container's width by default.
27
+ **Everything extra is opt-in.** A bare `<DataGrid columns={...} rows={...} />` is a plain read-only table. Sorting, the search box, page info, selection, resizing and the rest only appear when you switch them on:
28
+
29
+ | Feature | How you switch it on | Default |
30
+ | --- | --- | --- |
31
+ | Sorting | `sortable: true` **on a column** | off |
32
+ | Multi-column sort | `multiSort` on the grid | off |
33
+ | Global search box | `searchable` | off |
34
+ | Pagination bar (row count, page controls) | `pagination` | off |
35
+ | Column filter | `filterable: true` on a column | off |
36
+ | Editing | `editable` | off |
37
+ | Row selection | `rowSelection` | off |
38
+ | Server-side mode | `serverSide` | off |
39
+ | Resize / pin | `resizable: true` / `pinned` on a column | off |
40
+ | Show / hide columns button | `columnMenu` | off |
41
+ | Arrow-key navigation | `keyboardNavigation` | off |
42
+ | CSV export button | `exportable` | off |
43
+ | Virtualization | `virtualize` | off |
44
+ | Theme | `theme` | none (plain look) |
45
+
46
+ Everything is written in TypeScript, ships its own type definitions, and uses plain CSS (no CSS-in-JS runtime). The grid is 100% of its container's width by default. There is deliberately **no column reordering**.
27
47
 
28
48
  ---
29
49
 
@@ -47,6 +67,32 @@ The library does not inject CSS by itself. If the grid looks unstyled, this impo
47
67
 
48
68
  ## 3. Quick start
49
69
 
70
+ A read-only table, the smallest possible grid:
71
+
72
+ ```tsx
73
+ import { DataGrid, type ColumnDef } from "react-gridix";
74
+ import "react-gridix/style.css";
75
+
76
+ interface Person { id: number; name: string; city: string }
77
+
78
+ const columns: ColumnDef<Person>[] = [
79
+ { field: "id", headerName: "ID" },
80
+ { field: "name", headerName: "Name" },
81
+ { field: "city", headerName: "City" },
82
+ ];
83
+
84
+ const rows: Person[] = [
85
+ { id: 1, name: "Amina", city: "Kampala" },
86
+ { id: 2, name: "Brian", city: "Nairobi" },
87
+ ];
88
+
89
+ export default function App() {
90
+ return <DataGrid columns={columns} rows={rows} />;
91
+ }
92
+ ```
93
+
94
+ The same grid with the common features switched on:
95
+
50
96
  ```tsx
51
97
  import { useState } from "react";
52
98
  import { DataGrid, type ColumnDef } from "react-gridix";
@@ -55,20 +101,21 @@ import "react-gridix/style.css";
55
101
  interface Person {
56
102
  id: number;
57
103
  name: string;
58
- contact: { email: string; phone: string };
104
+ joined: string; // "2026-03-14"
105
+ contact: { email: string };
59
106
  }
60
107
 
61
108
  const columns: ColumnDef<Person>[] = [
62
- { field: "id", headerName: "ID", type: "number", editable: false },
63
- { field: "name", headerName: "Name", filterable: true },
109
+ { field: "id", headerName: "ID", type: "number", editable: false, sortable: true },
110
+ { field: "name", headerName: "Name", sortable: true, filterable: true },
111
+ { field: "joined", headerName: "Joined", type: "date", sortable: true, filterable: true },
64
112
  { field: "contact.email", headerName: "Email" },
65
- { field: "contact.phone", headerName: "Phone" },
66
113
  ];
67
114
 
68
115
  export default function App() {
69
116
  const [rows, setRows] = useState<Person[]>([
70
- { id: 1, name: "Amina", contact: { email: "amina@example.com", phone: "+256 700 100 200" } },
71
- { id: 2, name: "Brian", contact: { email: "brian@example.com", phone: "+256 700 100 237" } },
117
+ { id: 1, name: "Amina", joined: "2026-03-14", contact: { email: "amina@example.com" } },
118
+ { id: 2, name: "Brian", joined: "2025-11-02", contact: { email: "brian@example.com" } },
72
119
  ]);
73
120
 
74
121
  return (
@@ -78,6 +125,9 @@ export default function App() {
78
125
  getRowId={(r) => r.id}
79
126
  editable
80
127
  onRowsChange={setRows}
128
+ searchable // search box
129
+ pagination // row count and page controls
130
+ rowSelection // checkbox column
81
131
  theme="ocean"
82
132
  />
83
133
  );
@@ -100,19 +150,51 @@ Pass a theme name to the grid. **Without a `theme` prop nothing changes**: the g
100
150
  | --- | --- |
101
151
  | `"ocean"` | Light, cool blues, blue gradient header, soft shadow |
102
152
  | `"midnight"` | Dark slate, indigo accent, dark popups and inputs |
103
- | `"sunset"` | Warm cream, orange-to-pink gradient header, serif font, rounded corners |
153
+ | `"sunset"` | Warm cream, burnt-orange-to-raspberry gradient header, serif font, rounded corners |
104
154
  | `"forest"` | Calm greens, solid dark-green header, sharper corners |
155
+ | `"lavender"` | Light purple, deep violet gradient header, rounded corners |
156
+ | `"coffee"` | Warm browns and cream, serif font, rich brown header |
157
+ | `"graphite"` | Dark neutral grey with a teal accent, flat and sharp |
158
+ | `"aurora"` | Deep night blue with an emerald-to-blue-to-violet gradient header and a soft glow |
159
+
160
+ A theme is only a set of CSS variables on `[data-rg-theme="name"]`, written with `:where()` (zero specificity). So you can still override any variable or any class on top of a theme. Popups (the filter popup, the column menu) are drawn in `document.body`, and the grid copies the theme attribute onto them, so they follow the theme too.
161
+
162
+ **Switching themes at run time** is just a state change:
163
+
164
+ ```tsx
165
+ const [theme, setTheme] = useState<GridTheme>("ocean");
166
+
167
+ <select value={theme} onChange={(e) => setTheme(e.target.value as GridTheme)}>
168
+ {["ocean", "midnight", "sunset", "forest", "lavender", "coffee", "graphite", "aurora"].map((t) => (
169
+ <option key={t}>{t}</option>
170
+ ))}
171
+ </select>
172
+ <DataGrid theme={theme} ... />
173
+ ```
174
+
175
+ **Following the system dark mode.** Choose the theme from a media query:
176
+
177
+ ```tsx
178
+ const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
179
+ <DataGrid theme={dark ? "graphite" : "lavender"} ... />
180
+ ```
105
181
 
106
- A theme is only a set of CSS variables on `[data-rk-theme="name"]`, written with `:where()` (zero specificity). So you can still override any variable or any class on top of a theme. The filter popup is drawn in `document.body`, and the grid copies the theme attribute onto it, so the popup follows the theme too.
182
+ **Tweaking a built-in theme.** Because themes have zero specificity, any variable or rule you write wins over them:
183
+
184
+ ```css
185
+ /* make the aurora header a single colour */
186
+ [data-rg-theme="aurora"] { --rg-header-bg: #0f766e; }
187
+ ```
107
188
 
108
189
  **Making your own theme.** Any other string works as a theme name. Define the variables you want:
109
190
 
110
191
  ```css
111
- [data-rk-theme="brand"] {
112
- --rk-header-bg: #7c3aed;
113
- --rk-header-text: #fff;
114
- --rk-row-hover: #f5f3ff;
115
- --rk-primary: #7c3aed;
192
+ [data-rg-theme="brand"] {
193
+ --rg-header-bg: #7c3aed;
194
+ --rg-header-text: #fff;
195
+ --rg-row-hover: #f5f3ff;
196
+ --rg-selected-bg: #ede9fe; /* selected rows */
197
+ --rg-primary: #7c3aed;
116
198
  }
117
199
  ```
118
200
 
@@ -120,56 +202,70 @@ A theme is only a set of CSS variables on `[data-rk-theme="name"]`, written with
120
202
  <DataGrid theme="brand" ... />
121
203
  ```
122
204
 
205
+ A dark custom theme should also set the colours that the light defaults assume, for example `--rg-surface`, `--rg-text`, `--rg-border`, `--rg-pinned-bg`, `--rg-skeleton-a` and `--rg-skeleton-b`. Copy the `graphite` block in `themes.css` as a starting point.
206
+
123
207
  ### 4.2 CSS variables
124
208
 
125
209
  Every variable is optional and is read with a fallback, so nothing is declared on the grid itself. Set them on `:root`, on any parent element, in your own theme, or in the grid's `style`.
126
210
 
127
211
  | Variable | Default | Used for |
128
212
  | --- | --- | --- |
129
- | `--rk-font` | system UI stack | Font family |
130
- | `--rk-text` | `#111827` | Main text |
131
- | `--rk-muted` | `#6b7280` | Secondary text, empty state |
132
- | `--rk-border` | `#d1d5db` | Borders |
133
- | `--rk-surface` | `#ffffff` | Background of the grid, inputs, buttons |
134
- | `--rk-bg-subtle` | `#f3f4f6` | Header and footer background |
135
- | `--rk-primary` | `#4f46e5` | Focus rings, active editor border |
136
- | `--rk-danger` | `#dc2626` | Invalid editor border |
137
- | `--rk-invalid-bg` | `#fef2f2` | Invalid editor background |
138
- | `--rk-grid-bg` | `--rk-surface` | Grid background |
139
- | `--rk-grid-radius` | `10px` | Grid corner radius |
140
- | `--rk-grid-shadow` | `none` | Grid shadow |
141
- | `--rk-toolbar-bg` | `transparent` | Toolbar background |
142
- | `--rk-input-bg`, `--rk-input-border`, `--rk-input-text` | surface, border, inherit | Search box, filter box, editor, page controls |
143
- | `--rk-header-bg` | `--rk-bg-subtle` | Header background. Any CSS background, so gradients work |
144
- | `--rk-header-text` | inherit | Header text colour |
145
- | `--rk-header-hover-bg` | `rgba(0,0,0,.06)` | Header hover |
146
- | `--rk-header-border` | `--rk-border` | Line under the header |
147
- | `--rk-cell-border` | `#eef0f3` | Lines between rows |
148
- | `--rk-row-hover` | `#f3f4f6` | Row hover |
149
- | `--rk-stripe-a`, `--rk-stripe-b` | `transparent`, `#fafafa` | Alternating row colours (see [stripes](#striped-rows)) |
150
- | `--rk-footer-bg`, `--rk-footer-text` | subtle, muted | Pagination bar |
151
- | `--rk-btn-hover` | `#e5e7eb` | Page button and Clear button hover |
152
- | `--rk-filter-active` | `--rk-primary` | Filter dot colour while a filter is set |
153
- | `--rk-filter-glow` | `rgba(79,70,229,.25)` | Ring around an active filter dot |
154
- | `--rk-popup-bg`, `--rk-popup-text`, `--rk-popup-border` | white, dark, grey | Filter popup |
155
- | `--rk-popup-radius`, `--rk-popup-shadow` | `10px`, soft shadow | Filter popup shape |
156
- | `--rk-row-duration` | `0.38s` | Row entrance animation length |
157
- | `--rk-row-stagger` | `28ms` | Extra delay per row in the entrance animation |
158
- | `--rk-checkbox-size` | `20px` | Checkbox width and height |
159
- | `--rk-checkbox-border-width` | `1px` | Checkbox border width |
160
- | `--rk-checkbox-radius` | `0` | Checkbox corner radius (no rounding by default) |
161
- | `--rk-checkbox-color` | `#4f46e5` | Checked colour (also the focus ring and the native accent) |
162
- | `--rk-checkbox-border` | `#9ca3af` | Border colour of an unchecked `"button"` / `"tick"` checkbox |
163
- | `--rk-checkbox-bg` | `#ffffff` | Background of an unchecked `"button"` / `"tick"` checkbox |
164
- | `--rk-checkbox-tick` | `#ffffff` | Colour of the tick, and of a custom character in the `"tick"` variant |
213
+ | `--rg-font` | system UI stack | Font family |
214
+ | `--rg-text` | `#111827` | Main text |
215
+ | `--rg-muted` | `#6b7280` | Secondary text, empty state |
216
+ | `--rg-border` | `#d1d5db` | Borders |
217
+ | `--rg-surface` | `#ffffff` | Background of the grid, inputs, buttons |
218
+ | `--rg-bg-subtle` | `#f3f4f6` | Header and footer background |
219
+ | `--rg-primary` | `#4f46e5` | Focus rings, active editor border |
220
+ | `--rg-danger` | `#dc2626` | Invalid editor border |
221
+ | `--rg-invalid-bg` | `#fef2f2` | Invalid editor background |
222
+ | `--rg-grid-bg` | `--rg-surface` | Grid background |
223
+ | `--rg-grid-radius` | `10px` | Grid corner radius |
224
+ | `--rg-grid-shadow` | `none` | Grid shadow |
225
+ | `--rg-toolbar-bg` | `transparent` | Toolbar background |
226
+ | `--rg-input-bg`, `--rg-input-border`, `--rg-input-text` | surface, border, inherit | Search box, filter inputs, editor, page controls |
227
+ | `--rg-header-bg` | `--rg-bg-subtle` | Header background. Any CSS background, so gradients work |
228
+ | `--rg-header-text` | inherit | Header text colour |
229
+ | `--rg-header-hover-bg` | `rgba(0,0,0,.06)` | Header hover |
230
+ | `--rg-header-border` | `--rg-border` | Line under the header |
231
+ | `--rg-cell-border` | `#eef0f3` | Lines between rows |
232
+ | `--rg-row-hover` | `#f3f4f6` | Row hover |
233
+ | `--rg-selected-bg` | `#eef2ff` | Background of selected rows |
234
+ | `--rg-on-primary` | `#ffffff` | Text colour on top of `--rg-primary` (the selected day in the calendar). Dark themes set a dark colour |
235
+ | `--rg-pinned-bg` | `--rg-grid-bg` | Opaque background behind pinned body cells (so scrolled cells do not show through) |
236
+ | `--rg-pinned-header-bg` | `--rg-bg-subtle` | Opaque background behind pinned header cells |
237
+ | `--rg-skeleton-a`, `--rg-skeleton-b` | `#e5e7eb`, `#f3f4f6` | The two colours of the shimmering loading bars |
238
+ | `--rg-stripe-a`, `--rg-stripe-b` | `transparent`, `#fafafa` | Alternating row colours (see [stripes](#striped-rows)) |
239
+ | `--rg-footer-bg`, `--rg-footer-text` | subtle, muted | Pagination bar |
240
+ | `--rg-btn-hover` | `#e5e7eb` | Page button, toolbar button and Clear button hover |
241
+ | `--rg-filter-active` | `--rg-primary` | Filter dot colour while a filter is set |
242
+ | `--rg-filter-glow` | `rgba(79,70,229,.25)` | Ring around an active filter dot |
243
+ | `--rg-popup-bg`, `--rg-popup-text`, `--rg-popup-border` | white, dark, grey | Filter popup and column menu |
244
+ | `--rg-popup-radius`, `--rg-popup-shadow` | `10px`, soft shadow | Popup shape |
245
+ | `--rg-row-duration` | `0.38s` | Row entrance animation length |
246
+ | `--rg-row-stagger` | `28ms` | Extra delay per row in the entrance animation |
247
+ | `--rg-checkbox-size` | `17px` | Checkbox width and height |
248
+ | `--rg-checkbox-border-width` | `1px` | Checkbox border width |
249
+ | `--rg-checkbox-radius` | `0` | Checkbox corner radius (square by default). The inner mark of the `"button"` variant is rounded half as much. The sunset and lavender themes use `2px` |
250
+ | `--rg-checkbox-color` | `#4f46e5` | Checked colour (also the focus ring and the native accent) |
251
+ | `--rg-checkbox-border` | `#9ca3af` | Border colour of an unchecked `"button"` / `"tick"` checkbox |
252
+ | `--rg-checkbox-bg` | `#ffffff` | Background of an unchecked `"button"` / `"tick"` checkbox |
253
+ | `--rg-checkbox-tick` | `#ffffff` | Colour of the tick, and of a custom character in the `"tick"` variant |
165
254
 
166
255
  ```css
167
256
  :root {
168
- --rk-primary: #0d9488;
169
- --rk-row-hover: #f0fdfa;
257
+ --rg-primary: #0d9488;
258
+ --rg-row-hover: #f0fdfa;
259
+ --rg-selected-bg: #ccfbf1;
170
260
  }
171
261
  ```
172
262
 
263
+ Scoped to one grid through its `style` prop:
264
+
265
+ ```tsx
266
+ <DataGrid style={{ "--rg-header-bg": "#0f172a", "--rg-header-text": "#fff" } as React.CSSProperties} ... />
267
+ ```
268
+
173
269
  ---
174
270
 
175
271
  ## 5. Checkbox
@@ -221,16 +317,16 @@ A checkbox in three looks. Every variant toggles with **one click** (or the Spac
221
317
  />
222
318
  ```
223
319
 
224
- (`aria-checked:` works for the `"button"` and `"tick"` variants. The native input has no `aria-checked`, so use the `--rk-checkbox-color` variable for it.)
225
- 2. **CSS variables**: `--rk-checkbox-size`, `--rk-checkbox-border-width`, `--rk-checkbox-radius`, `--rk-checkbox-color`, `--rk-checkbox-border`, `--rk-checkbox-bg` and `--rk-checkbox-tick` (see [Theming](#42-css-variables)). Set them anywhere above the checkbox:
320
+ (`aria-checked:` works for the `"button"` and `"tick"` variants. The native input has no `aria-checked`, so use the `--rg-checkbox-color` variable for it.)
321
+ 2. **CSS variables**: `--rg-checkbox-size`, `--rg-checkbox-border-width`, `--rg-checkbox-radius`, `--rg-checkbox-color`, `--rg-checkbox-border`, `--rg-checkbox-bg` and `--rg-checkbox-tick` (see [Theming](#42-css-variables)). Set them anywhere above the checkbox:
226
322
 
227
323
  ```tsx
228
- <Checkbox styles={{ root: { "--rk-checkbox-color": "#dc2626" } as CSSProperties }} ... />
324
+ <Checkbox styles={{ root: { "--rg-checkbox-color": "#dc2626" } as CSSProperties }} ... />
229
325
  ```
230
- 3. **A custom character**, for example `indicator="★"` (any text, emoji or React node). It shows when the box is checked, in the `"button"` and `"tick"` variants. The default fill is dropped; the character is sized to the box (70% of `--rk-checkbox-size`) and coloured with `--rk-checkbox-color` in the button variant and `--rk-checkbox-tick` in the tick variant. Restyle it through `classNames.indicator` / `styles.indicator`.
231
- 4. **Plain CSS** with these stable classes: `.rk-checkbox`, `.rk-checkbox--button`, `.rk-checkbox--tick`, `.rk-checkbox--native`, `.rk-checkbox--checked`, `.rk-checkbox--indeterminate`, `.rk-checkbox--disabled`, `.rk-checkbox__control`, `.rk-checkbox__indicator`, `.rk-checkbox__indicator--custom` and `.rk-checkbox__label`. State is also exposed as `data-state="checked"`, `"unchecked"` or `"indeterminate"` and, on the button variants, `aria-checked` (`"true"`, `"false"` or `"mixed"`).
326
+ 3. **A custom character**, for example `indicator="★"` (any text, emoji or React node). It shows when the box is checked, in the `"button"` and `"tick"` variants. The default fill is dropped; the character is sized to the box (70% of `--rg-checkbox-size`) and coloured with `--rg-checkbox-color` in the button variant and `--rg-checkbox-tick` in the tick variant. Restyle it through `classNames.indicator` / `styles.indicator`.
327
+ 4. **Plain CSS** with these stable classes: `.rg-checkbox`, `.rg-checkbox--button`, `.rg-checkbox--tick`, `.rg-checkbox--native`, `.rg-checkbox--checked`, `.rg-checkbox--indeterminate`, `.rg-checkbox--disabled`, `.rg-checkbox__control`, `.rg-checkbox__indicator`, `.rg-checkbox__indicator--custom` and `.rg-checkbox__label`. State is also exposed as `data-state="checked"`, `"unchecked"` or `"indeterminate"` and, on the button variants, `aria-checked` (`"true"`, `"false"` or `"mixed"`).
232
328
 
233
- **Defaults:** a 1px border and square corners (`--rk-checkbox-border-width: 1px`, `--rk-checkbox-radius: 0`). Round them with `--rk-checkbox-radius: 999px` or a Tailwind `rounded-*` class on `classNames.control`.
329
+ **Defaults:** a 1px border and square corners (`--rg-checkbox-border-width: 1px`, `--rg-checkbox-radius: 0`). Round them with `--rg-checkbox-radius: 999px` or a Tailwind `rounded-*` class on `classNames.control`.
234
330
 
235
331
  The checkbox CSS is written with `:where()` (zero specificity), so any class you add wins without `!important`.
236
332
 
@@ -250,44 +346,106 @@ The checkbox CSS is written with `:where()` (zero specificity), so any class you
250
346
 
251
347
  - **`rows`** is an array of plain objects.
252
348
  - **`columns`** is an array of column definitions. Each one names a `field` (a key of the row object) and describes how to show, sort, filter and edit it.
253
- - **Row ids.** Pass `getRowId` when your rows have a stable id. Without it the grid uses each row's index in `rows`, which is fine for static data.
349
+ - **Row ids.** Pass `getRowId` when your rows have a stable id. Without it the grid uses each row's index in `rows`, which is fine for static data. Selection, server mode and virtualization all work best with real ids.
254
350
  - **`rowIndex`** in callbacks and params is always the row's index in your original `rows` array, not its position on screen.
255
- - **The grid keeps its own copy of the rows** so edits appear immediately. It re-syncs whenever the `rows` prop changes identity. See [section 6.8](#68-editing).
351
+ - **The grid keeps its own copy of the rows** so edits appear immediately. It re-syncs whenever the `rows` prop changes identity. See [section 6.9](#69-editing).
352
+ - **Controlled or uncontrolled.** Search, filters, sort, page and selection each work two ways. Leave the prop out and the grid keeps the state itself (optionally starting from a `default…` value). Pass the prop together with its `on…Change` callback and you own the state. See [6.14](#614-controlled-state).
353
+ - **Opt-in everywhere.** Features are off until you switch them on (see the table in the [overview](#1-overview)).
256
354
 
257
- The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` checks that each `field` is a real key of `T` or a dot path into a nested object (see [nested fields](#611-nested-fields)).
355
+ The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` checks that each `field` is a real key of `T` or a dot path into a nested object (see [nested fields](#612-nested-fields)).
258
356
 
259
357
  ### 6.2 DataGrid props
260
358
 
359
+ **Data and editing**
360
+
261
361
  | Prop | Type | Default | Description |
262
362
  | --- | --- | --- | --- |
263
363
  | `columns` | `ColumnDef<T>[]` | required | Column definitions |
264
- | `rows` | `T[]` | required | Data |
364
+ | `rows` | `T[]` | required | Data. In `serverSide` mode: the current page |
265
365
  | `getRowId` | `(row, index) => string \| number` | row index | Stable id for each row |
266
366
  | `editable` | `boolean` | `false` | Allow editing cells. Columns can override this |
267
367
  | `enterMovesDown` | `boolean` | `true` | After Enter saves a cell, edit the cell below |
268
368
  | `onCellEdit` | `(event: CellEditEvent<T>) => void` | | Called after a cell edit is saved |
269
369
  | `onRowsChange` | `(rows: T[]) => void` | | Called with the full updated array after an edit |
270
- | `sortable` | `boolean` | `true` | Click headers to sort. Columns can override |
271
- | `defaultSort` | `SortModel \| null` | `null` | Initial sort. Read once, on mount |
272
- | `onSortChange` | `(sort: SortModel \| null) => void` | | Called when the sort changes |
273
- | `searchable` | `boolean` | `true` | Show the global search box |
274
- | `searchPlaceholder` | `string` | `"Search…"` | Placeholder of the search box |
275
- | `pagination` | `boolean` | `true` | Split rows into pages |
276
- | `paginationPosition` | `"bottom" \| "top" \| "both"` | `"bottom"` | Where the row count and page controls go |
277
- | `pageSize` | `number` | `10` | Initial rows per page. Read once, on mount |
278
- | `pageSizeOptions` | `number[]` | `[5, 10, 25, 50]` | Choices in the page size menu. The current page size is added if missing |
279
370
  | `onRowClick` | `(row, rowIndex) => void` | | Called when a row is clicked |
371
+
372
+ **Row selection** (see [6.13](#613-row-selection))
373
+
374
+ | Prop | Type | Default | Description |
375
+ | --- | --- | --- | --- |
376
+ | `rowSelection` | `boolean \| "single" \| "multiple"` | `false` | Adds a checkbox column. `true` means `"multiple"` |
377
+ | `selectedRowIds` | `(string \| number)[]` | | Controlled selection |
378
+ | `defaultSelectedRowIds` | `(string \| number)[]` | `[]` | Initial selection when not controlled |
379
+ | `onSelectionChange` | `(selection: RowSelection<T>) => void` | | Called with `{ ids, rows }` |
380
+ | `isRowSelectable` | `(row) => boolean` | all rows | Return `false` to disable a row's checkbox |
381
+
382
+ **Sorting** (see [6.4](#64-sorting))
383
+
384
+ | Prop | Type | Default | Description |
385
+ | --- | --- | --- | --- |
386
+ | `multiSort` | `boolean` | `false` | Shift+click adds more sort columns |
387
+ | `sort` | `SortModel[]` | | Controlled sort |
388
+ | `defaultSort` | `SortModel \| SortModel[] \| null` | `null` | Initial sort when not controlled |
389
+ | `onSortChange` | `(sort: SortModel[]) => void` | | Called when the sort changes |
390
+
391
+ **Search, filters and paging** (see [6.5](#65-searching-and-filtering), [6.7](#67-pagination), [6.14](#614-controlled-state))
392
+
393
+ | Prop | Type | Default | Description |
394
+ | --- | --- | --- | --- |
395
+ | `searchable` | `boolean` | `false` | Show the global search box |
396
+ | `searchPlaceholder` | `string` | `"Search…"` | Placeholder of the search box |
397
+ | `search` / `onSearchChange` | `string` / `(s) => void` | | Controlled search text |
398
+ | `filters` / `onFiltersChange` | `Record<string, FilterValue>` / `(f) => void` | | Controlled column filters |
399
+ | `pagination` | `boolean` | `false` | Split rows into pages and show the bar with the row count and page controls |
400
+ | `paginationPosition` | `"bottom" \| "top" \| "both"` | `"bottom"` | Where the bar goes |
401
+ | `pageSize` | `number` | `10` | Rows per page (initial value) |
402
+ | `pageSizeOptions` | `number[]` | `[5, 10, 25, 50]` | Choices in the page size menu. The current size is added if missing |
403
+ | `page` / `onPageChange` | `number` / `(page) => void` | | Controlled page (zero-based) |
404
+ | `onPageSizeChange` | `(pageSize) => void` | | Called when the page size menu changes |
405
+
406
+ **Server-side mode, loading and errors** (see [6.15](#615-server-side-mode), [6.16](#616-loading-skeleton))
407
+
408
+ | Prop | Type | Default | Description |
409
+ | --- | --- | --- | --- |
410
+ | `serverSide` | `boolean` | `false` | Your server sorts, filters, searches and pages |
411
+ | `rowCount` | `number` | | Total rows on the server (for the page count) |
412
+ | `onQueryChange` | `(query: GridQuery) => void \| Promise<unknown>` | | Called on mount and whenever the query changes. A returned promise drives the loading state |
413
+ | `queryDebounce` | `number` | `250` | Milliseconds to wait after the last keystroke in the search box before querying |
414
+ | `filterDebounce` | `number` | `300` | Milliseconds to wait after the last keystroke in a typed column filter (text, exact, number) before it is applied. A column can override it. See [Debouncing typed filters](#debouncing-typed-filters) |
415
+ | `loading` | `boolean` | follows the promise | Force the loading state on or off |
416
+ | `loadingRows` | `number` | page size (max 20), or 5 | How many skeleton rows to draw |
417
+ | `skeletonRow` | `ComponentType<SkeletonRowProps<T>>` | shimmer bars | Your own component for each placeholder row |
418
+ | `errorMessage` | `ReactNode` | `"Could not load the data."` | Shown with a Retry button when the promise rejects |
419
+
420
+ **Columns, keyboard, big data and export**
421
+
422
+ | Prop | Type | Default | Description |
423
+ | --- | --- | --- | --- |
424
+ | `columnMenu` | `boolean` | `false` | A "Columns" button to show and hide columns |
425
+ | `onColumnVisibilityChange` | `(hiddenFields: string[]) => void` | | Called with the fields that are hidden |
426
+ | `keyboardNavigation` | `boolean` | `false` | Arrow keys, Home, End, Page Up and Page Down move between cells |
427
+ | `virtualize` | `boolean` | `false` | Render only the visible rows |
428
+ | `rowHeight` | `number` | `40` | Row height in px when virtualizing |
429
+ | `overscan` | `number` | `6` | Extra rows drawn above and below the window |
430
+ | `exportable` | `boolean \| ExportOptions` | `false` | Show an "Export CSV" button |
431
+ | `apiRef` | `MutableRefObject<DataGridApi<T> \| null>` | | Call `exportCsv()`, `getSelectedRows()` and more from outside |
432
+
433
+ **Look and feel**
434
+
435
+ | Prop | Type | Default | Description |
436
+ | --- | --- | --- | --- |
437
+ | `theme` | `GridTheme \| string` | | Applies a theme. Nothing changes without it. See [Theming](#4-theming) |
280
438
  | `toolbar` | `ReactNode` | | Extra content on the right of the toolbar |
281
439
  | `emptyMessage` | `ReactNode` | `"No rows to show"` | Shown when no rows match |
282
440
  | `striped` | `boolean` | `false` | Alternate row shading |
283
441
  | `stripeColors` | `[string, string]` | | Two colours for the alternating rows. Implies `striped`. See [striped rows](#striped-rows) |
284
442
  | `animateRows` | `boolean` | `true` | Rows slide up and fade in when they render. See [row animation](#row-animation) |
285
- | `theme` | `"ocean" \| "midnight" \| "sunset" \| "forest" \| string` | | Applies a theme. Nothing changes without it. See [Theming](#4-theming) |
286
- | `checkboxVariant` | `"button" \| "tick" \| "native"` | `"native"` | Checkbox look for `type: "boolean"` columns. Columns can override it. See [Checkbox columns](#checkbox-columns-type-boolean) |
443
+ | `rowAnimationClassName` | `string \| (row, rowIndex) => string` | | Your own CSS class for the entrance animation. Replaces the built-in slide-up. See [custom row animation](#custom-row-animation) |
444
+ | `checkboxVariant` | `"button" \| "tick" \| "native"` | `"native"` | Checkbox look for boolean columns and row selection. Columns can override it |
287
445
  | `maxHeight` | `number \| string` | | Makes the body scroll, with a sticky header. Example: `400` or `"60vh"` |
288
446
  | `className` | `string` | | Classes for the root element (Tailwind works). The grid is `width: 100%` of its container by default |
289
447
  | `style` | `CSSProperties` | | Inline styles for the root element |
290
- | `classNames` | `Partial<Record<GridSlot, string>>` | | Classes for individual parts. See [6.10](#610-styling) |
448
+ | `classNames` | `Partial<Record<GridSlot, string>>` | | Classes for individual parts. See [6.11](#611-styling) |
291
449
  | `styles` | `Partial<Record<GridSlot, CSSProperties>>` | | Inline styles for individual parts |
292
450
  | `rowClassName` | `string \| (row, rowIndex) => string` | | Classes for rows |
293
451
  | `rowStyle` | `CSSProperties \| (row, rowIndex) => CSSProperties` | | Inline styles for rows |
@@ -296,8 +454,9 @@ The grid is generic: `DataGrid<T>` infers `T` from `rows`, and `ColumnDef<T>` ch
296
454
 
297
455
  ```ts
298
456
  const columns: ColumnDef<Employee>[] = [
299
- { field: "name", headerName: "Name" },
300
- { field: "salary", headerName: "Salary", type: "number", align: "right" },
457
+ { field: "name", headerName: "Name", sortable: true, filterable: true },
458
+ { field: "salary", headerName: "Salary", type: "number", align: "right", sortable: true },
459
+ { field: "hired", headerName: "Hired", type: "date", sortable: true, filterable: true },
301
460
  ];
302
461
  ```
303
462
 
@@ -305,57 +464,143 @@ const columns: ColumnDef<Employee>[] = [
305
464
  | --- | --- | --- |
306
465
  | `field` | `FieldPath<T>` | **Required.** Key of the row object this column reads and writes. Use dot notation for nested objects: `"contact.phone"` |
307
466
  | `headerName` | `string` | Header text. Defaults to the field name, except in `type: "boolean"` columns, where it defaults to an empty string |
308
- | `type` | `"text" \| "number" \| "boolean"` | `"number"` columns edit and parse as numbers. `"boolean"` columns show a checkbox. Default `"text"` |
467
+ | `type` | `"text" \| "number" \| "boolean" \| "date" \| "sequence"` | `"number"` columns edit and parse as numbers, `"boolean"` columns show a checkbox, `"date"` columns have a calendar picker (see [6.6](#66-date-columns)), `"sequence"` columns show row numbers (see [row numbers](#row-numbers-sequence-column)). Default `"text"` |
468
+ | `sequenceStart` | `number` | Only for `type: "sequence"`: the number of the first row (default `1`) |
469
+ | `sequenceRestartEachPage` | `boolean` | Only for `type: "sequence"`: start again at `sequenceStart` on every page (default `false`: the count continues, 11, 12, ...) |
309
470
  | `width` | `number \| string` | Header cell width |
310
- | `minWidth` | `number \| string` | Header cell minimum width |
471
+ | `minWidth` / `maxWidth` | `number \| string` / `number` | Smallest and largest width (`maxWidth` limits dragging when resizing) |
311
472
  | `align` | `"left" \| "center" \| "right"` | Text alignment for the header, cells and editor |
312
- | `sortable` | `boolean` | Overrides the grid-level `sortable` |
473
+ | `sortable` | `boolean` | **Opt in to sorting this column** (default `false`) |
474
+ | `resizable` | `boolean` | Show a drag handle on the header's right edge (default `false`). See [6.17](#617-resize-pin-and-hide-columns) |
475
+ | `pinned` | `"left" \| "right"` | Stick the column to an edge while scrolling sideways |
476
+ | `hidden` | `boolean` | Start hidden (shown again from the Columns menu) |
477
+ | `hideable` | `boolean` | Set `false` to keep a column out of the Columns menu (default `true`) |
313
478
  | `filterable` | `boolean` | Show a filter indicator in this column's header (default `false`). Clicking it opens the filter popup. See [6.5](#65-searching-and-filtering) |
314
- | `filterPlaceholder` | `string` | Placeholder of the default filter text box |
479
+ | `filterType` | `"text" \| "exact" \| "number" \| "date" \| "select"` | Which filter controls the popup shows. Default follows `type` |
480
+ | `filterOptions` | `(string \| FilterOption)[]` | Choices for a `"select"` filter. Without it they are built from the data |
481
+ | `filterPlaceholder` | `string` | Placeholder of the text filter box |
482
+ | `filterDebounce` | `number` | Milliseconds to wait after typing in **this** column's text / exact / number filter before it is applied. Overrides the grid's `filterDebounce` (default 300). `0` applies on every keystroke |
315
483
  | `filterIndicator` | `ComponentType<FilterIndicatorProps<T>>` | Your own component drawn instead of the default dot |
316
- | `filterIndicatorClassName` | `string` | Classes for this column's indicator button |
317
- | `filterIndicatorStyle` | `CSSProperties` | Inline styles for this column's indicator button |
318
- | `filterPopupClassName` | `string` | Classes for this column's filter popup |
319
- | `filterPopupStyle` | `CSSProperties` | Inline styles for this column's filter popup |
484
+ | `filterIndicatorClassName` / `filterIndicatorStyle` | `string` / `CSSProperties` | Classes and inline styles for this column's indicator button |
485
+ | `filterPopupClassName` / `filterPopupStyle` | `string` / `CSSProperties` | Classes and inline styles for this column's filter popup |
486
+ | `filterRenderer` | `ComponentType<FilterRendererProps<T>>` | Component that replaces the filter controls inside the popup |
320
487
  | `editable` | `boolean \| (params: CellEditableParams<T>) => boolean` | Overrides the grid-level `editable`. A function decides per cell |
321
- | `valueFormatter` | `(value, row) => string` | Text used for display, search, filtering. Does not change the stored value |
488
+ | `valueFormatter` | `(value, row) => string` | Text used for display, search and text filters. Does not change the stored value |
322
489
  | `valueParser` | `(text, row) => unknown` | Turns typed text into the value to store |
323
490
  | `validate` | `(value, row) => boolean` | Return `false` to reject an edit |
324
491
  | `comparator` | `(a, b, rowA, rowB) => number` | Custom sort order |
492
+ | `dateFormat` / `dateLocale` | `Intl.DateTimeFormatOptions` / `string` | How a `"date"` column is displayed |
493
+ | `dateMin` / `dateMax` | `string` (`"YYYY-MM-DD"`) | Earliest and latest day the date picker accepts |
494
+ | `exportValue` | `(value, row) => string \| number \| boolean \| null` | What goes in the CSV file for this column |
325
495
  | `cellRenderer` | `ComponentType<CellParams<T>>` | Component that renders the cell |
326
- | `cellEditor` | `ComponentType<CellEditorProps<T>>` | Component used instead of the text box when editing |
496
+ | `cellEditor` | `ComponentType<CellEditorProps<T>>` | Component used instead of the built-in editor |
327
497
  | `headerRenderer` | `ComponentType<HeaderRendererProps<T>>` | Component that renders the header label |
328
- | `filterRenderer` | `ComponentType<FilterRendererProps<T>>` | Component that replaces the filter text box inside the popup |
329
- | `className` | `string` | Classes for the whole column: its header cell and every body cell (Tailwind works) |
330
- | `style` | `CSSProperties` | Inline styles for the whole column: its header cell and every body cell |
331
- | `headerClassName` | `string` | Classes for this column's header cell only |
332
- | `headerStyle` | `CSSProperties` | Inline styles for this column's header cell |
333
- | `cellClassName` | `string \| (params) => string` | Classes for this column's cells |
334
- | `cellStyle` | `CSSProperties \| (params) => CSSProperties` | Inline styles for this column's cells |
498
+ | `className` / `style` | `string` / `CSSProperties` | Classes and inline styles for the whole column: header cell and every body cell (Tailwind works) |
499
+ | `headerClassName` / `headerStyle` | `string` / `CSSProperties` | Header cell only |
500
+ | `cellClassName` / `cellStyle` | `string \| (params) => string` / `CSSProperties \| (params) => CSSProperties` | Body cells only |
335
501
  | `checkbox` | `ColumnCheckboxOptions` | Look of the checkbox in a `type: "boolean"` column: `variant`, `indicator`, `className`, `style`, `classNames`, `styles` |
336
- | `headerCheckbox` | `boolean` | On a `type: "boolean"` column, draws a select-all checkbox in the header, labelled with the header name (no label when it is empty). See [Select-all header checkbox](#select-all-header-checkbox) |
502
+ | `headerCheckbox` | `boolean` | On a `type: "boolean"` column, draws a select-all checkbox in the header. See [Select-all header checkbox](#select-all-header-checkbox) |
503
+
504
+ #### Row numbers (sequence column)
505
+
506
+ `type: "sequence"` adds a column of row numbers. It needs no data in your rows: give it a `field` that starts with `#` (so it can never clash with a real field) and a header.
507
+
508
+ ```tsx
509
+ const columns: ColumnDef<Person>[] = [
510
+ { field: "#", type: "sequence", headerName: "No.", width: 64, align: "center" },
511
+ { field: "name", sortable: true, filterable: true },
512
+ { field: "email" },
513
+ ];
514
+ ```
515
+
516
+ - The number is the row's **position in the list the user is looking at**. After a search or a filter the numbers start again at 1, and after sorting they follow the new order.
517
+ - With pagination the count **continues** on the next pages (page 2 starts at 11). Set `sequenceRestartEachPage: true` to start at `sequenceStart` on every page.
518
+ - `sequenceStart` changes the first number: `sequenceStart: 0` counts from 0.
519
+ - In server-side mode the number is `page * pageSize + position`, so page 3 of 10 rows starts at 21.
520
+ - The column is display-only: it cannot be sorted, filtered or edited, and it is skipped by the search. It can be pinned (`pinned: "left"`), resized and hidden like any other. The header defaults to `#`.
521
+ - `valueFormatter` works on it: `valueFormatter: (n) => String(n).padStart(3, "0")` gives `001`, `002`, ...
522
+ - CSV export includes it. A `scope: "page"` export numbers like the page does.
523
+
524
+ ```tsx
525
+ // "No. 1", "No. 2", ... pinned on the left, starting after a header row of your own
526
+ { field: "#", type: "sequence", sequenceStart: 1, pinned: "left", width: 70, valueFormatter: (n) => `No. ${n}` }
527
+
528
+ // restart on every page
529
+ { field: "#", type: "sequence", sequenceRestartEachPage: true }
530
+ ```
337
531
 
338
532
  ### 6.4 Sorting
339
533
 
534
+ **Sorting is chosen per column.** A column is sortable only when its definition says `sortable: true`. There is no grid-level switch.
535
+
536
+ ```tsx
537
+ const columns: ColumnDef<Employee>[] = [
538
+ { field: "id", type: "number" }, // not sortable
539
+ { field: "name", sortable: true }, // click to sort
540
+ { field: "salary", type: "number", sortable: true },
541
+ ];
542
+ ```
543
+
340
544
  - Click a header to sort ascending, click again for descending, click a third time to clear the sort.
341
- - Only one column is sorted at a time.
342
- - Numbers sort numerically, dates by time, booleans false before true. Everything else sorts as text with natural number ordering (`"item 2"` before `"item 10"`), ignoring case.
545
+ - Numbers sort numerically, dates by day, booleans false before true. Everything else sorts as text with natural number ordering (`"item 2"` before `"item 10"`), ignoring case.
343
546
  - Empty values (`null`, `undefined`, `""`) always sort last, in both directions.
344
- - Add a `comparator` to sort any other way:
547
+ - Headers are keyboard accessible (Tab to a header, Enter or Space to sort) and expose `aria-sort`. A column that is not sortable gets no sort arrow, no pointer cursor and no focus stop.
548
+
549
+ **Custom order** with a `comparator`:
550
+
551
+ ```tsx
552
+ const order = ["low", "medium", "high", "urgent"];
553
+ { field: "priority", sortable: true, comparator: (a, b) => order.indexOf(a) - order.indexOf(b) }
554
+ ```
555
+
556
+ **Starting sorted** with `defaultSort`:
557
+
558
+ ```tsx
559
+ <DataGrid defaultSort={{ field: "salary", direction: "desc" }} ... />
560
+ ```
561
+
562
+ **Multi-column sort.** Switch on `multiSort`. A plain click still replaces the sort with that one column. **Shift+click** adds the column as the next tie-breaker (or cycles its direction if it is already in the sort). Small numbers in the headers show the priority.
563
+
564
+ ```tsx
565
+ <DataGrid
566
+ multiSort
567
+ defaultSort={[
568
+ { field: "department", direction: "asc" },
569
+ { field: "salary", direction: "desc" }, // inside each department, highest salary first
570
+ ]}
571
+ columns={columns}
572
+ rows={rows}
573
+ />
574
+ ```
575
+
576
+ **Reacting to the sort** (always an array, empty when unsorted):
345
577
 
346
578
  ```tsx
347
- { field: "priority", comparator: (a, b) => order.indexOf(a) - order.indexOf(b) }
579
+ <DataGrid
580
+ onSortChange={(sort) => console.log(sort)}
581
+ // [{ field: "department", direction: "asc" }, { field: "salary", direction: "desc" }]
582
+ ...
583
+ />
348
584
  ```
349
585
 
350
- - Set an initial sort with `defaultSort={{ field: "salary", direction: "desc" }}` and react to changes with `onSortChange`.
351
- - Headers are keyboard accessible (Tab to a header, Enter or Space to sort) and expose `aria-sort`.
352
- - Turn sorting off for the whole grid with `sortable={false}` or per column with `sortable: false`.
586
+ **Controlling the sort yourself** (also see [6.14](#614-controlled-state)):
587
+
588
+ ```tsx
589
+ const [sort, setSort] = useState<SortModel[]>([{ field: "name", direction: "asc" }]);
590
+
591
+ <button onClick={() => setSort([])}>Reset sort</button>
592
+ <DataGrid sort={sort} onSortChange={setSort} ... />
593
+ ```
353
594
 
354
595
  ### 6.5 Searching and filtering
355
596
 
356
- **Global search.** The box above the grid matches rows where any column's displayed text contains the typed text (case-insensitive). "Displayed text" means the result of `valueFormatter` if there is one.
597
+ **Global search.** Off by default. Switch it on with `searchable` and a box appears above the grid. It matches rows where any column's displayed text contains the typed text (case-insensitive). "Displayed text" means the result of `valueFormatter` if there is one.
598
+
599
+ ```tsx
600
+ <DataGrid searchable searchPlaceholder="Search people…" ... />
601
+ ```
357
602
 
358
- **Column filters.** Filters are set per column, in the column definition. Only columns with `filterable: true` get one:
603
+ **Column filters.** Filters are set per column. Only columns with `filterable: true` get one:
359
604
 
360
605
  ```tsx
361
606
  const columns: ColumnDef<Person>[] = [
@@ -366,13 +611,84 @@ const columns: ColumnDef<Person>[] = [
366
611
  ```
367
612
 
368
613
  - A filterable column shows a small **indicator** in its header, next to the label. By default it is a dot. It is dim while no filter is set, and lit (with a glow) while one is active.
369
- - **Clicking the indicator** opens a popup right below it with a text box. The popup is not there until you click. It closes on Escape, on a click outside, or on Enter in the text box. Clicking the indicator does not sort the column.
614
+ - **Clicking the indicator** opens a popup right below it. The popup is not there until you click. It closes on Escape, on a click outside, or on Enter in a text box. Clicking the indicator does not sort the column.
370
615
  - The popup shows a **Clear** button while a filter is set.
371
- - Filters work as a case-insensitive "contains" on the displayed text, and combine with each other and with the global search.
616
+ - All filters and the global search combine (a row must pass every one).
372
617
  - The popup is drawn in `document.body` and positioned next to the indicator, so a scrolling grid never clips it. It follows scrolling and resizing, and stays inside the window.
373
- - There is no grid-level filter prop: a column without `filterable: true` has no filter, and a filter value is ignored if its column stops being filterable.
618
+ - There is no grid-level filter prop to turn filters on: a column without `filterable: true` has no filter, and a filter value is ignored if its column stops being filterable.
619
+
620
+ #### Filter types
621
+
622
+ Each column's filter has a kind. By default it follows the column `type`; set `filterType` to choose another.
623
+
624
+ | `filterType` | Default for | Popup shows | Rows pass when |
625
+ | --- | --- | --- | --- |
626
+ | `"text"` | `type: "text"` | One text box | The displayed text **contains** the typed text (case-insensitive) |
627
+ | `"exact"` | | One text box | The displayed text **equals** the typed text (case-insensitive) |
628
+ | `"number"` | `type: "number"` | Min and Max boxes | The number is between Min and Max (either may be left empty). Rows without a number never match |
629
+ | `"date"` | `type: "date"` | From and To boxes with a range calendar | The day is between From and To (either may be left empty) |
630
+ | `"select"` | `type: "boolean"` | A checkbox per value | The raw value is one of the ticked values |
631
+
632
+ ```tsx
633
+ const columns: ColumnDef<Task>[] = [
634
+ // "contains": typing "log" finds "Fix login" and "Catalogue"
635
+ { field: "title", filterable: true },
636
+
637
+ // "exact": typing "active" finds "active" but NOT "inactive"
638
+ { field: "status", filterable: true, filterType: "exact" },
639
+
640
+ // number range: type: "number" gives a Min / Max popup by default
641
+ { field: "budget", type: "number", filterable: true },
642
+
643
+ // date range: type: "date" gives From / To boxes and a calendar by default
644
+ { field: "due", type: "date", filterable: true },
645
+
646
+ // choose from a list: values are collected from the data...
647
+ { field: "owner", filterable: true, filterType: "select" },
648
+
649
+ // ...or you decide the list and the labels
650
+ {
651
+ field: "priority",
652
+ filterable: true,
653
+ filterType: "select",
654
+ filterOptions: [
655
+ { value: "p1", label: "Urgent" },
656
+ { value: "p2", label: "Normal" },
657
+ "p3", // a plain string is its own label
658
+ ],
659
+ },
660
+ ];
661
+ ```
662
+
663
+ Notes on `select`: when `filterOptions` is left out, the choices are the distinct values in the data, sorted. A boolean column gets "Yes" and "No". Ticking several values combines them with OR.
664
+
665
+ #### Reading and setting filters
666
+
667
+ The shape of a filter value depends on its kind:
668
+
669
+ ```ts
670
+ // text / exact: "sales"
671
+ // number: { min: 1000, max: 5000 } (either key may be missing)
672
+ // date: { from: "2026-01-01", to: "2026-03-31" }
673
+ // select: ["active", "pending"]
674
+ ```
675
+
676
+ Listen with `onFiltersChange`, or control the filters completely:
677
+
678
+ ```tsx
679
+ const [filters, setFilters] = useState<Record<string, FilterValue>>({
680
+ status: ["open", "blocked"], // select
681
+ budget: { min: 1000 }, // number
682
+ due: { from: "2026-06-01" }, // date
683
+ });
684
+
685
+ <button onClick={() => setFilters({})}>Clear all filters</button>
686
+ <DataGrid filters={filters} onFiltersChange={setFilters} ... />
687
+ ```
374
688
 
375
- **Styling each indicator separately.** Every column has its own options:
689
+ #### Styling each indicator
690
+
691
+ Every column has its own options:
376
692
 
377
693
  ```tsx
378
694
  {
@@ -384,9 +700,11 @@ const columns: ColumnDef<Person>[] = [
384
700
  }
385
701
  ```
386
702
 
387
- The dot takes its colour from the button's text colour, so `color` (or `text-*`) restyles it. The colour while a filter is active comes from `--rk-filter-active`. To style all indicators or popups at once, use `classNames.filterIndicator`, `classNames.filterPopup`, `classNames.filterInput` (or the matching `styles`).
703
+ The dot takes its colour from the button's text colour, so `color` (or `text-*`) restyles it. The colour while a filter is active comes from `--rg-filter-active`. To style all indicators or popups at once, use `classNames.filterIndicator`, `classNames.filterPopup`, `classNames.filterInput` (or the matching `styles`).
704
+
705
+ #### A custom indicator component
388
706
 
389
- **A custom indicator component.** Replace the dot with any component. It receives `{ column, active, open }`:
707
+ Replace the dot with any component. It receives `{ column, active, open }`:
390
708
 
391
709
  ```tsx
392
710
  function FunnelIndicator({ active }: FilterIndicatorProps<Employee>) {
@@ -402,49 +720,166 @@ function FunnelIndicator({ active }: FilterIndicatorProps<Employee>) {
402
720
 
403
721
  The component is drawn inside the indicator `<button>`, so do not put another button in it.
404
722
 
405
- **A custom filter control.** `filterRenderer` replaces the text box inside the popup (see [filterRenderer](#filterrenderer-custom-filter-control)).
723
+ #### A custom filter control
724
+
725
+ `filterRenderer` replaces **all** the built-in controls inside the popup, whatever the filter type (see [filterRenderer](#filterrenderer-custom-filter-control)). A custom control produces a text filter, so it works with the "contains" matching.
726
+
727
+ #### Debouncing typed filters
728
+
729
+ Typing in a text, exact or number filter does not apply the filter on every keystroke. The box updates at once, and the filter is applied once the user has stopped typing for a moment. This keeps a big grid responsive, and in [server-side mode](#615-server-side-mode) it means one request per pause instead of one per letter.
730
+
731
+ - The default wait is **300 ms**, set for the whole grid with `filterDebounce`.
732
+ - Any column can choose its own wait with `filterDebounce` in its definition (it wins over the grid's).
733
+ - `0` applies the filter on every keystroke.
734
+ - **Enter** applies a text or number filter immediately. Closing the popup also applies what is waiting, so typed text is never lost.
735
+ - **Clear**, or changing `filters` from outside, drops anything still waiting.
736
+ - Select checkboxes and date pickers are single clicks, so they apply at once. A custom `filterRenderer` also applies at once (debounce inside your component if you need it).
737
+ - The filter indicator lights up when the filter is applied, which is after the wait.
738
+
739
+ ```tsx
740
+ // the whole grid waits 500 ms
741
+ <DataGrid filterDebounce={500} ... />
742
+
743
+ const columns: ColumnDef<Person>[] = [
744
+ { field: "name", filterable: true }, // 500 ms (from the grid)
745
+ { field: "email", filterable: true, filterDebounce: 800 }, // slow typers: 800 ms
746
+ { field: "age", type: "number", filterable: true, filterDebounce: 0 }, // instant, a small local data set
747
+ { field: "status", filterable: true, filterType: "select" }, // clicks: always instant
748
+ ];
749
+ ```
750
+
751
+ For a server grid you will often raise it, for a local grid you can lower it:
752
+
753
+ ```tsx
754
+ <DataGrid serverSide filterDebounce={600} queryDebounce={400} onQueryChange={load} ... />
755
+ ```
406
756
 
407
- Notes:
757
+ #### Notes
408
758
 
409
- - Searching and filtering use the displayed text, so a salary shown as `$52,000` is found by `52,000` and not by `52000` unless your formatter says so.
410
- - Filtering is a substring match. Filtering a status column for `active` also matches `inactive`. For exact choices, use a `filterRenderer` and a distinct display text, or format the values so they don't overlap.
759
+ - Searching and text filters use the displayed text, so a salary shown as `$52,000` is found by `52,000` and not by `52000` unless your formatter says so. Number, date and select filters look at the raw value instead, so formatting never gets in their way.
411
760
  - Changing the search, a filter, the sort or the page size sends the grid back to page 1.
412
- - Turn off the search box with `searchable={false}`. Leave out `filterable` (or set it to `false`) to give a column no filter.
761
+ - In [server-side mode](#615-server-side-mode) the filters are not applied by the grid: they are handed to your server in the query.
762
+
763
+ ### 6.6 Date columns
764
+
765
+ `type: "date"` is a built-in column type with formatting, a date picker editor, chronological sorting and a From / To filter.
766
+
767
+ ```tsx
768
+ interface Task { id: number; title: string; due: string; created: Date }
769
+
770
+ const columns: ColumnDef<Task>[] = [
771
+ { field: "title", editable: true },
772
+ {
773
+ field: "due",
774
+ type: "date",
775
+ sortable: true,
776
+ filterable: true,
777
+ editable: true,
778
+ dateMin: "2026-01-01", // the picker will not accept earlier days
779
+ dateMax: "2027-12-31",
780
+ },
781
+ ];
782
+
783
+ <DataGrid columns={columns} rows={tasks} editable onRowsChange={setTasks} />
784
+ ```
785
+
786
+ **What a row may hold.** The cell understands `"2026-11-05"`, a full ISO string (`"2026-11-05T10:30:00Z"`, whose date part is used as written), a `Date` object, a timestamp number, or any string `Date.parse` understands. Empty values show blank and sort last. Text that is not a date is shown as it is.
787
+
788
+ **Editing.** Double-click a date cell (or press Enter / F2). A text box opens with a **calendar** under it:
789
+
790
+ - Click a day to save it at once. **Today** picks today and **Clear** saves an empty date (`null`).
791
+ - Or type the date as `YYYY-MM-DD`. The calendar follows what you type, and Enter (or a click elsewhere) saves it. Tab saves and moves on, Escape cancels.
792
+ - The arrows change month (`‹ ›`) and year (`« »`). Days before `dateMin` or after `dateMax` are dimmed and cannot be picked.
793
+ - Text that is not a real date (such as `2026-02-31`) marks the editor invalid and is not saved. Clicking away from it throws it away.
794
+ - Typing letters does not start an editor on a date cell: use double-click, Enter or F2.
795
+ - The calendar is drawn by the library, so it looks the same in every browser and follows your theme (`--rg-popup-*`, `--rg-primary`, `--rg-on-primary`). Month and weekday names follow `dateLocale`.
796
+
797
+ ```tsx
798
+ { field: "due", type: "date", editable: true, dateLocale: "fr-FR", dateMin: "2026-01-01" }
799
+ ```
800
+
801
+ **The saved value keeps the shape the row already had.**
413
802
 
414
- ### 6.6 Pagination
803
+ | Row held | Saved value |
804
+ | --- | --- |
805
+ | `"2026-03-14"` (text) | `"2026-03-14"` |
806
+ | a `Date` object | a `Date` at local midnight |
807
+ | a timestamp number | a timestamp at local midnight |
808
+ | empty | the same shape as the other rows in the column (the ISO text when there are none) |
809
+
810
+ Dates are handled in the **user's local time zone**, so typing 5 November never turns into 4 November because of UTC.
811
+
812
+ **Display.** The default is the browser's medium date format (`Nov 5, 2026`). Change it with `dateLocale` and `dateFormat` (any `Intl.DateTimeFormat` options):
813
+
814
+ ```tsx
815
+ { field: "created", type: "date", dateLocale: "en-GB", dateFormat: { dateStyle: "long" } } // 5 November 2026
816
+ { field: "created", type: "date", dateLocale: "de-DE", dateFormat: { day: "2-digit", month: "2-digit", year: "numeric" } } // 05.11.2026
817
+ ```
818
+
819
+ A `valueFormatter` still wins over these two if you set one.
820
+
821
+ **Sorting and filtering.** Dates sort by day (earliest first, empties last). The filter popup has From and To pickers; either may be empty for an open-ended range:
415
822
 
416
823
  ```tsx
417
- <DataGrid pageSize={20} pageSizeOptions={[10, 20, 50, 100]} paginationPosition="both" />
824
+ <DataGrid filters={{ due: { from: "2026-03-01", to: "2026-06-30" } }} onFiltersChange={setFilters} ... />
418
825
  ```
419
826
 
420
- The pagination bar shows the row range ("11–20 of 200"), a rows-per-page menu, first/previous/next/last buttons and "Page x of y".
827
+ **The date filter calendar.** The filter popup of a date column has From and To boxes and a calendar. Type the dates, or click on the calendar: the first click sets **From**, the second sets **To** (swapped if it is earlier), and a third click starts a new range. Days in the range are highlighted. With the keyboard, Tab into the calendar, then arrows move a day or a week, Page Up / Page Down change month, Home / End go to the start / end of the week, Enter picks.
828
+
829
+ **CSV export** writes dates as `"YYYY-MM-DD"` unless you give `exportValue`.
830
+
831
+ ### 6.7 Pagination
832
+
833
+ Pagination is off by default. Switch it on to split the rows into pages and show the bar with the row range ("11–20 of 200"), a rows-per-page menu, first / previous / next / last buttons and "Page x of y". **The bar (and its page information) only appears when `pagination` is passed.**
834
+
835
+ ```tsx
836
+ <DataGrid pagination pageSize={20} pageSizeOptions={[10, 20, 50, 100]} paginationPosition="both" ... />
837
+ ```
421
838
 
422
839
  | `paginationPosition` | Result |
423
840
  | --- | --- |
424
841
  | `"bottom"` | Bar under the table (default) |
425
- | `"top"` | Bar above the table, below the search box |
842
+ | `"top"` | Bar above the table, below the toolbar |
426
843
  | `"both"` | Both bars, kept in sync |
427
844
 
428
- With `pagination={false}` every row is rendered and only the row count shows, at the bottom. Combine it with `maxHeight` to get a scrolling grid with a sticky header:
845
+ Without `pagination` every row is rendered and there is no footer at all. Combine it with `maxHeight` for a scrolling grid with a sticky header:
429
846
 
430
847
  ```tsx
431
- <DataGrid pagination={false} maxHeight={400} />
848
+ <DataGrid maxHeight={400} ... />
432
849
  ```
433
850
 
434
- `pageSize` is only read when the grid first mounts. The user's choice in the menu is then kept inside the grid.
851
+ `pageSize` is only read when the grid first mounts. The user's choice in the menu is then kept inside the grid. To own the page, see [6.14](#614-controlled-state).
852
+
853
+ **Reacting to paging:**
854
+
855
+ ```tsx
856
+ <DataGrid
857
+ pagination
858
+ onPageChange={(page) => console.log("page", page)} // zero-based
859
+ onPageSizeChange={(size) => localStorage.setItem("size", String(size))}
860
+ pageSize={Number(localStorage.getItem("size")) || 10}
861
+ ...
862
+ />
863
+ ```
435
864
 
436
- ### 6.7 Row click, toolbar and empty state
865
+ ### 6.8 Row click, toolbar and empty state
437
866
 
438
867
  ```tsx
439
868
  <DataGrid
440
869
  onRowClick={(row, rowIndex) => navigate(`/people/${row.id}`)}
441
- toolbar={<button onClick={exportCsv}>Export</button>}
870
+ toolbar={<button onClick={openNewPersonDialog}>Add person</button>}
442
871
  emptyMessage="Nothing matches your search"
443
872
  striped
444
873
  />
445
874
  ```
446
875
 
447
- ### 6.8 Editing
876
+ The toolbar row is drawn when at least one of `searchable`, `columnMenu`, `exportable` or `toolbar` is used. Your `toolbar` content sits on the right, next to the built-in buttons. To style a toolbar button the same way as the built-in ones, give it the class `rg-grid__tool-btn`:
877
+
878
+ ```tsx
879
+ toolbar={<button className="rg-grid__tool-btn" onClick={refetch}>Reload</button>}
880
+ ```
881
+
882
+ ### 6.9 Editing
448
883
 
449
884
  Turn editing on with `editable` on the grid, then lock or unlock columns:
450
885
 
@@ -491,7 +926,7 @@ Details:
491
926
  2. Otherwise, in a `type: "number"` column, an empty text stores `null`, a valid number stores that number, and anything else is rejected.
492
927
  3. Otherwise the text is stored as a string.
493
928
 
494
- **Validation.** After parsing, `validate(value, row)` is called. If it returns `false` (or a number could not be parsed), the value is not saved, the editor stays open, and it gets the `rk-grid__editor--invalid` class and `aria-invalid="true"`. Pressing Esc or clicking away discards the invalid text.
929
+ **Validation.** After parsing, `validate(value, row)` is called. If it returns `false` (or a number could not be parsed), the value is not saved, the editor stays open, and it gets the `rg-grid__editor--invalid` class and `aria-invalid="true"`. Pressing Esc or clicking away discards the invalid text.
495
930
 
496
931
  ```tsx
497
932
  { field: "email", validate: (v) => /^\S+@\S+\.\S+$/.test(String(v)) }
@@ -531,7 +966,7 @@ Important: the grid copies `rows` into its own state and re-syncs whenever the `
531
966
  />
532
967
  ```
533
968
 
534
- ### 6.9 Custom components per column
969
+ ### 6.10 Custom components per column
535
970
 
536
971
  Four hooks let you replace parts of a column with your own React components. They are rendered as real components, so they can use hooks and state.
537
972
 
@@ -638,7 +1073,7 @@ function DepartmentFilter({ value, onChange }: FilterRendererProps<Employee>) {
638
1073
 
639
1074
  #### Checkbox columns (`type: "boolean"`)
640
1075
 
641
- Set `type: "boolean"` on a column and each cell shows a [Checkbox](#checkbox). One click saves the new `true` / `false` value.
1076
+ Set `type: "boolean"` on a column and each cell shows a [Checkbox](#5-checkbox). One click saves the new `true` / `false` value.
642
1077
 
643
1078
  ```tsx
644
1079
  const columns: ColumnDef<Task>[] = [
@@ -660,7 +1095,7 @@ How it behaves:
660
1095
  - **No text editor.** Boolean cells never open the text editor, and Enter and Tab navigation skip them. A click on the checkbox does not trigger `onRowClick`.
661
1096
  - **Sorting, search and filters.** Unchecked sorts before checked. Search and filters match the text `Yes` / `No`, or whatever your `valueFormatter` returns. A missing value (`null`, `undefined`) shows unchecked.
662
1097
  - **Header label.** The header of a boolean column is empty unless you set `headerName`. Screen readers still get the field name.
663
- - **Custom character.** `checkbox: { indicator: "★" }` draws that character instead of the default fill or tick when checked. Colour it with `--rk-checkbox-color` (button variant) or `--rk-checkbox-tick` (tick variant), for example through `checkbox.styles.root`.
1098
+ - **Custom character.** `checkbox: { indicator: "★" }` draws that character instead of the default fill or tick when checked. Colour it with `--rg-checkbox-color` (button variant) or `--rg-checkbox-tick` (tick variant), for example through `checkbox.styles.root`.
664
1099
  - **Defaults.** 1px border, no rounded corners. See [Checkbox](#5-checkbox).
665
1100
  - **A column with its own `cellEditor`** is treated as a normal editable column instead.
666
1101
 
@@ -672,7 +1107,7 @@ How it behaves:
672
1107
  | One column | `checkbox: { variant: "tick" }` |
673
1108
  | Whole grid, classes or inline styles | `classNames` / `styles` slots `checkbox`, `checkboxControl`, `checkboxIndicator` |
674
1109
  | One column, classes or inline styles | `checkbox: { className, style, classNames, styles, indicator }` (same parts as the standalone `Checkbox`) |
675
- | Colours and size | CSS variables such as `--rk-checkbox-color`, set on the grid's `className` / `style`, or per column in `checkbox.styles.root` |
1110
+ | Colours and size | CSS variables such as `--rg-checkbox-color`, set on the grid's `className` / `style`, or per column in `checkbox.styles.root` |
676
1111
 
677
1112
  ```tsx
678
1113
  <DataGrid
@@ -727,7 +1162,7 @@ Add `headerCheckbox: true` to a boolean column. The header label becomes a check
727
1162
  - **Sorting:** clicking the checkbox or its label does not sort. Click the sort arrow beside it (or the empty part of the header cell) to sort.
728
1163
  - **Custom header:** if the column also has a `headerRenderer`, that is used and `headerCheckbox` is ignored. `headerCheckbox` does nothing on columns that are not `type: "boolean"`.
729
1164
 
730
- ### 6.10 Styling
1165
+ ### 6.11 Styling
731
1166
 
732
1167
  #### Whole grid
733
1168
 
@@ -762,6 +1197,8 @@ Add `headerCheckbox: true` to a boolean column. The header label becomes a check
762
1197
  | `pageInfo` | The "Page 1 of 20" text |
763
1198
  | `pageSize` | The rows-per-page `<select>` |
764
1199
  | `pagination` | Wrapper around the page-size menu and buttons |
1200
+ | `toolButton` | Toolbar buttons (Columns, Export CSV, Retry) |
1201
+ | `skeleton` | The shimmering bars of the loading skeleton |
765
1202
  | `pageButton` | Each first/previous/next/last button |
766
1203
  | `checkbox` | Root of each checkbox in a `type: "boolean"` column |
767
1204
  | `checkboxControl` | The checkbox's button or input |
@@ -826,7 +1263,7 @@ When both are given, `style` wins over `styles.root`.
826
1263
  <DataGrid stripeColors={["#fef3c7", "#e0f2fe"]} /> // your two colours (implies striped)
827
1264
  ```
828
1265
 
829
- Rows alternate between the two colours, starting with the first. `stripeColors` sets `--rk-stripe-a` and `--rk-stripe-b` on the grid. You can also set those variables in your own CSS or theme, and turn on `striped`. Hovering a row still highlights it (`--rk-row-hover`).
1266
+ Rows alternate between the two colours, starting with the first. `stripeColors` sets `--rg-stripe-a` and `--rg-stripe-b` on the grid. You can also set those variables in your own CSS or theme, and turn on `striped`. Hovering a row still highlights it (`--rg-row-hover`).
830
1267
 
831
1268
  #### Row animation
832
1269
 
@@ -836,7 +1273,32 @@ When rows render (the first load, a new page, a sort, a search or a filter), the
836
1273
  <DataGrid animateRows={false} /> // turn it off
837
1274
  ```
838
1275
 
839
- Tune it with `--rk-row-duration` (default `0.38s`) and `--rk-row-stagger` (default `28ms` per row, up to 24 rows). Changing page or sort also scrolls the grid's own scroll area smoothly back to the top. Visitors who ask their system for reduced motion get no animation (`prefers-reduced-motion`).
1276
+ Tune it with `--rg-row-duration` (default `0.38s`) and `--rg-row-stagger` (default `28ms` per row, up to 24 rows). Changing page or sort also scrolls the grid's own scroll area smoothly back to the top. Visitors who ask their system for reduced motion get no animation (`prefers-reduced-motion`).
1277
+
1278
+ #### Custom row animation
1279
+
1280
+ Pass your own CSS class to replace the built-in slide-up. The class should set `animation`. Every row also gets the CSS variable `--rg-i` (its position on the page, 0 to 24), so a delay of `calc(var(--rg-i) * 40ms)` makes rows arrive one after another.
1281
+
1282
+ ```css
1283
+ @keyframes fade-left { from { opacity: 0; transform: translateX(-24px); } to { opacity: 1; transform: none; } }
1284
+ .row-fade-left { animation: fade-left 0.45s ease-out both; animation-delay: calc(var(--rg-i, 0) * 45ms); }
1285
+ ```
1286
+
1287
+ ```tsx
1288
+ <DataGrid rowAnimationClassName="row-fade-left" ... />
1289
+ ```
1290
+
1291
+ It can also be a function of the row, for example to animate new rows differently:
1292
+
1293
+ ```tsx
1294
+ <DataGrid rowAnimationClassName={(row) => (row.isNew ? "row-flash" : "row-fade-left")} ... />
1295
+ ```
1296
+
1297
+ With Tailwind, define the keyframes in your config and use the utility, for example `rowAnimationClassName="animate-fade-in-left [animation-delay:calc(var(--rg-i)*40ms)]"`.
1298
+
1299
+ - It runs on the same moments as the built-in animation: first render, a new page, sort, search or filter.
1300
+ - `animateRows={false}` turns every row animation off, custom or not. It is also skipped while `virtualize` is on.
1301
+ - Reduced motion: the built-in animation stops for people who ask for it. Your own class is yours to handle: wrap it in `@media (prefers-reduced-motion: no-preference) { ... }`.
840
1302
 
841
1303
  #### Width
842
1304
 
@@ -848,23 +1310,35 @@ If you prefer writing CSS, these class names are stable.
848
1310
 
849
1311
  | Class | Element |
850
1312
  | --- | --- |
851
- | `.rk-grid` | Root |
852
- | `.rk-grid__toolbar`, `.rk-grid__search` | Toolbar and search box |
853
- | `.rk-grid__scroll`, `.rk-grid__table`, `.rk-grid__thead`, `.rk-grid__tbody` | Table structure |
854
- | `.rk-grid__tr` | Every row |
855
- | `.rk-grid__th`, `.rk-grid__th--sortable`, `.rk-grid__th-content`, `.rk-grid__sort` | Header cells and sort arrow |
856
- | `.rk-grid__filter-btn`, `.rk-grid__filter-btn--active`, `.rk-grid__filter-btn--open`, `.rk-grid__filter-dot` | Filter indicator button and its default dot |
857
- | `.rk-grid-popup`, `.rk-grid__filter-input`, `.rk-grid__filter-clear` | Filter popup (in `document.body`), its text box and Clear button |
858
- | `.rk-grid__row`, `.rk-grid__row--stripe-a`, `.rk-grid__row--stripe-b`, `.rk-grid__row--enter`, `.rk-grid__row--clickable` | Body rows |
859
- | `.rk-grid__td`, `.rk-grid__td--editable`, `.rk-grid__td--editing` | Body cells |
860
- | `.rk-grid__editor`, `.rk-grid__editor--invalid`, `.rk-grid__custom-editor` | Editors |
861
- | `.rk-grid__empty` | Empty state |
862
- | `.rk-grid__checkbox-cell` | Wrapper around a checkbox in a boolean column (the checkbox's own classes start with `.rk-checkbox`, see [Checkbox](#checkbox)) |
863
- | `.rk-grid__footer`, `.rk-grid__footer--top` | Pagination bar (`data-position="top"` or `"bottom"`) |
864
- | `.rk-grid__count`, `.rk-grid__pagination`, `.rk-grid__page-size-label`, `.rk-grid__page-size`, `.rk-grid__page-btn`, `.rk-grid__page-info` | Pagination parts |
865
-
866
-
867
- ### 6.11 Nested fields
1313
+ | `.rg-grid` | Root |
1314
+ | `.rg-grid__toolbar`, `.rg-grid__search` | Toolbar and search box |
1315
+ | `.rg-grid__scroll`, `.rg-grid__table`, `.rg-grid__thead`, `.rg-grid__tbody` | Table structure |
1316
+ | `.rg-grid__tr` | Every row |
1317
+ | `.rg-grid__th`, `.rg-grid__th--sortable`, `.rg-grid__th-content`, `.rg-grid__sort` | Header cells and sort arrow |
1318
+ | `.rg-grid__filter-btn`, `.rg-grid__filter-btn--active`, `.rg-grid__filter-btn--open`, `.rg-grid__filter-dot` | Filter indicator button and its default dot |
1319
+ | `.rg-grid-popup`, `.rg-grid__filter-input`, `.rg-grid__filter-clear` | Filter popup (in `document.body`), its text box and Clear button |
1320
+ | `.rg-grid__row`, `.rg-grid__row--stripe-a`, `.rg-grid__row--stripe-b`, `.rg-grid__row--enter`, `.rg-grid__row--clickable` | Body rows |
1321
+ | `.rg-grid__td`, `.rg-grid__td--editable`, `.rg-grid__td--editing` | Body cells |
1322
+ | `.rg-grid__editor`, `.rg-grid__editor--invalid`, `.rg-grid__custom-editor` | Editors |
1323
+ | `.rg-grid__empty` | Empty state |
1324
+ | `.rg-grid__checkbox-cell` | Wrapper around a checkbox in a boolean column (the checkbox's own classes start with `.rg-checkbox`, see [Checkbox](#5-checkbox)) |
1325
+ | `.rg-grid__footer`, `.rg-grid__footer--top` | Pagination bar (`data-position="top"` or `"bottom"`) |
1326
+ | `.rg-grid__count`, `.rg-grid__pagination`, `.rg-grid__page-size-label`, `.rg-grid__page-size`, `.rg-grid__page-btn`, `.rg-grid__page-info` | Pagination parts |
1327
+ | `.rg-grid__toolbar-left`, `.rg-grid__toolbar-actions` | The two halves of the toolbar (search and selected count / buttons) |
1328
+ | `.rg-grid__tool-btn`, `.rg-grid__selected-count` | Toolbar buttons and the "N selected" label |
1329
+ | `.rg-grid__select-cell`, `.rg-grid__row--selected` | Row-selection checkbox cell and a selected row |
1330
+ | `.rg-grid__pinned`, `.rg-grid__pinned--left`, `.rg-grid__pinned--right`, `.rg-grid__pinned--edge` | Pinned cells (the edge class marks the last left / first right one) |
1331
+ | `.rg-grid__resizer` | The column resize handle |
1332
+ | `.rg-grid__sort-order` | The priority number next to a sort arrow in multi-column sort |
1333
+ | `.rg-grid--virtual`, `.rg-grid__spacer` | Virtualized grid and its spacer rows |
1334
+ | `.rg-grid__skeleton`, `.rg-grid__skeleton-row`, `.rg-grid__skeleton-cell` | Loading placeholder bar, row and (custom) cell |
1335
+ | `.rg-cal`, `.rg-cal__head`, `.rg-cal__nav`, `.rg-cal__title`, `.rg-cal__grid`, `.rg-cal__dow`, `.rg-cal__foot`, `.rg-cal__link` | The calendar: frame, month bar and arrows, weekday names, footer buttons |
1336
+ | `.rg-cal__day`, `.rg-cal__day--today`, `.rg-cal__day--selected`, `.rg-cal__day--in-range`, `.rg-cal__day--range-start`, `.rg-cal__day--range-end`, `.rg-cal__day--outside` | A day button and its states |
1337
+ | `.rg-grid__error` | The error message cell (with the Retry button) |
1338
+ | `.rg-grid__filter-fields`, `.rg-grid__filter-field`, `.rg-grid__filter-options`, `.rg-grid__filter-empty` | Controls inside the filter popup (number, date, select) |
1339
+
1340
+
1341
+ ### 6.12 Nested fields
868
1342
 
869
1343
  A column's `field` can point into nested objects with dot notation:
870
1344
 
@@ -892,152 +1366,903 @@ const columns: ColumnDef<Person>[] = [
892
1366
  - `__proto__`, `prototype` and `constructor` are never read or written through a path.
893
1367
  - `getRowId`, `onRowClick` and other callbacks work as before.
894
1368
 
895
- ### 6.12 Keyboard reference
1369
+ ### 6.13 Row selection
896
1370
 
897
- | Where | Key | Action |
898
- | --- | --- | --- |
899
- | Header | Enter / Space | Sort by this column |
900
- | Selected cell | Enter / F2 | Start editing |
901
- | Selected cell | Any character | Start editing, replacing the value |
902
- | Editor | Enter | Save, edit cell below |
903
- | Editor | Shift+Enter | Save, edit cell above |
904
- | Editor | Tab / Shift+Tab | Save, edit next / previous cell |
905
- | Editor | Esc | Cancel |
1371
+ Off by default. `rowSelection` adds a checkbox column on the left.
906
1372
 
907
- Arrow-key navigation between cells is not implemented. Use Tab, or the mouse, to select a cell.
1373
+ ```tsx
1374
+ <DataGrid columns={columns} rows={rows} getRowId={(r) => r.id} rowSelection />
1375
+ ```
908
1376
 
909
- ### 6.13 Accessibility
1377
+ | Value | Behaviour |
1378
+ | --- | --- |
1379
+ | `true` or `"multiple"` | Many rows. The header has a select-all checkbox. Shift+click selects a range |
1380
+ | `"single"` | One row at a time (selecting another replaces it). No select-all checkbox |
910
1381
 
911
- - The table has `role="grid"`. Sortable headers are focusable, expose `aria-sort`, and work with Enter and Space.
912
- - Editable cells are focusable (`tabindex="0"`).
913
- - The search box, filter indicators (`aria-haspopup="dialog"`, `aria-expanded`), filter popups (`role="dialog"`), editors, page buttons and page-size menu have `aria-label`s. Checkboxes in columns with an empty header are named after the field.
914
- - Invalid editors set `aria-invalid="true"`.
915
- - Custom components you supply are responsible for their own accessibility.
1382
+ - Selected rows get the class `rg-grid__row--selected`, the colour `--rg-selected-bg` and `aria-selected="true"`.
1383
+ - Selection is kept **by row id**, so it survives paging, sorting, searching and filtering. Pass `getRowId` so ids are stable.
1384
+ - The **select-all checkbox** in the header acts on every selectable row that matches the current search and filters, **across all pages**. It shows a dash while only some of them are selected.
1385
+ - **Shift+click** on a row's checkbox selects (or clears) everything between it and the last row you clicked, as shown on screen. Disabled rows in between are skipped.
1386
+ - A "N selected" count appears in the toolbar (when a toolbar is shown) and in the pagination bar.
916
1387
 
917
- ### 6.14 TypeScript
1388
+ **Reading the selection** (`onSelectionChange` receives the ids and the row objects):
918
1389
 
919
- All types are exported:
1390
+ ```tsx
1391
+ function Page() {
1392
+ const [selection, setSelection] = useState<RowSelection<Person>>({ ids: [], rows: [] });
920
1393
 
921
- ```ts
922
- import type {
923
- ColumnDef,
924
- DataGridProps,
925
- CellParams,
926
- CellEditEvent,
927
- CellEditorProps,
928
- HeaderRendererProps,
929
- FilterRendererProps,
930
- GridSlot,
931
- SortModel,
932
- SortDirection,
933
- MoveDirection,
934
- PaginationPosition,
935
- CellEditableParams,
936
- ColumnCheckboxOptions,
937
- FilterIndicatorProps,
938
- FieldPath,
939
- GridTheme,
940
- CheckboxProps,
941
- CheckboxVariant,
942
- CheckboxSlot,
943
- } from "react-gridix";
1394
+ return (
1395
+ <>
1396
+ <button disabled={!selection.ids.length} onClick={() => deletePeople(selection.ids)}>
1397
+ Delete {selection.ids.length} people
1398
+ </button>
1399
+ <DataGrid
1400
+ columns={columns}
1401
+ rows={rows}
1402
+ getRowId={(r) => r.id}
1403
+ rowSelection
1404
+ onSelectionChange={setSelection}
1405
+ />
1406
+ </>
1407
+ );
1408
+ }
944
1409
  ```
945
1410
 
946
- Typing tips:
1411
+ **Controlled selection.** Pass `selectedRowIds` and keep it up to date from `onSelectionChange`:
947
1412
 
948
1413
  ```tsx
949
- const columns: ColumnDef<Employee>[] = [ ... ]; // field is checked against Employee (dot paths too)
950
- function Cell({ row }: CellParams<Employee>) { ... } // row is typed as Employee
1414
+ const [ids, setIds] = useState<(string | number)[]>([2, 4]);
1415
+
1416
+ <button onClick={() => setIds([])}>Clear</button>
1417
+ <DataGrid rowSelection selectedRowIds={ids} onSelectionChange={(s) => setIds(s.ids)} ... />
951
1418
  ```
952
1419
 
953
- ### 6.15 Recipes
1420
+ Or start with some rows selected and let the grid manage it: `defaultSelectedRowIds={[2, 4]}`.
954
1421
 
955
- **Read-only grid**
1422
+ **Rows that cannot be selected.** Return `false` from `isRowSelectable`. The row's checkbox is disabled, select-all skips it and Shift ranges pass over it:
956
1423
 
957
1424
  ```tsx
958
- <DataGrid columns={columns} rows={rows} /> // editable defaults to false
1425
+ <DataGrid rowSelection isRowSelectable={(row) => row.status !== "archived"} ... />
959
1426
  ```
960
1427
 
961
- **Currency column that accepts "$1,200" when typed**
1428
+ **Single selection**, for a master / detail layout:
962
1429
 
963
1430
  ```tsx
964
- {
965
- field: "salary",
966
- type: "number",
967
- align: "right",
968
- valueFormatter: (v) => (v == null ? "" : money.format(v)),
969
- valueParser: (text) => Number(text.replace(/[$,]/g, "")),
970
- validate: (v) => Number.isFinite(v),
971
- }
972
- ```
973
-
974
- (The editor opens with the raw number, such as `52000`. The parser only matters if the user types symbols.)
975
-
976
- **Only one column editable**
1431
+ const [current, setCurrent] = useState<Person | null>(null);
977
1432
 
978
- ```tsx
979
- <DataGrid columns={[{ field: "id" }, { field: "notes", editable: true }]} />
1433
+ <DataGrid
1434
+ rowSelection="single"
1435
+ getRowId={(r) => r.id}
1436
+ onSelectionChange={(s) => setCurrent(s.rows[0] ?? null)}
1437
+ ...
1438
+ />
1439
+ {current && <PersonDetails person={current} />}
980
1440
  ```
981
1441
 
982
- **Lock rows after approval**
1442
+ **From outside the grid**, with `apiRef` (see [6.19](#619-csv-export-and-the-api)):
983
1443
 
984
1444
  ```tsx
985
- { field: "amount", editable: ({ row }) => !row.approved }
1445
+ const api = useRef<DataGridApi<Person> | null>(null);
1446
+
1447
+ <button onClick={() => api.current?.selectAll()}>Select all</button>
1448
+ <button onClick={() => api.current?.clearSelection()}>Clear</button>
1449
+ <button onClick={() => console.log(api.current?.getSelectedRows())}>Log selection</button>
1450
+ <DataGrid rowSelection apiRef={api} ... />
986
1451
  ```
987
1452
 
988
- **Scrolling grid with sticky header**
1453
+ **Combined with other features.** Row selection works together with pinning (the checkbox column is pinned automatically when any column is pinned left), virtualization, keyboard navigation, and CSV export (`scope: "selected"`).
989
1454
 
990
- ```tsx
991
- <DataGrid pagination={false} maxHeight="60vh" />
992
- ```
1455
+ **In server-side mode** the grid only holds the current page, so it remembers every row it has seen. `onSelectionChange` and `getSelectedRows()` return those remembered rows, including ones selected on pages you have left. Select-all selects the rows of the page that is loaded.
993
1456
 
994
- **Highlight a row based on its data**
1457
+ ### 6.14 Controlled state
995
1458
 
996
- ```tsx
997
- <DataGrid rowClassName={(row) => (row.overdue ? "bg-red-50" : "")} />
998
- ```
1459
+ Search, filters, sort, page and selection can each be **uncontrolled** (the grid keeps the state; the default) or **controlled** (you pass the value and the change callback).
999
1460
 
1000
- ### 6.16 Known limitations
1461
+ | State | Value prop | Change callback | Initial value (uncontrolled) |
1462
+ | --- | --- | --- | --- |
1463
+ | Search text | `search` | `onSearchChange(text)` | |
1464
+ | Column filters | `filters` | `onFiltersChange(filters)` | |
1465
+ | Sort | `sort` | `onSortChange(sort)` | `defaultSort` |
1466
+ | Page (zero-based) | `page` | `onPageChange(page)` | page 0 |
1467
+ | Page size | | `onPageSizeChange(size)` | `pageSize` |
1468
+ | Selection | `selectedRowIds` | `onSelectionChange({ ids, rows })` | `defaultSelectedRowIds` |
1001
1469
 
1002
- Not included (yet):
1470
+ **Rules.**
1003
1471
 
1004
- - Column resizing, reordering, hiding or pinning
1005
- - Row selection as its own concept (a set of selected rows with `onSelectionChange`). True/false checkbox columns, including a select-all header (`headerCheckbox`), are supported, see [Checkbox columns](#checkbox-columns-type-boolean)
1006
- - Multi-column sorting
1007
- - Row virtualization (very large pages render every row, so keep `pageSize` moderate)
1008
- - Server-side sorting, filtering and paging (the grid works on the array you give it)
1009
- - Arrow-key navigation between cells
1010
- - Controlled search, filter or page state (only `defaultSort` and `pageSize` initial values)
1472
+ - Pass the value **and** its callback. A value without a callback makes that piece of state read-only.
1473
+ - You may control some pieces and leave the rest to the grid.
1474
+ - The page goes back to 0 automatically whenever the search, a filter, the sort or the page size changes. In a controlled grid you receive that through `onPageChange(0)`.
1011
1475
 
1012
- Behaviours to be aware of:
1476
+ **Example: keep the grid in sync with the URL** so a page can be bookmarked and the Back button works:
1013
1477
 
1014
- - Column filters match by substring, so `active` also matches `inactive`.
1015
- - Only one filter popup is open at a time (opening another closes the first by its outside click).
1016
- - Stripes and the row animation restart on every page, sort, search or filter change.
1017
- - `defaultSort` and `pageSize` are read once, on mount. Changing them later has no effect.
1018
- - `rows` must keep a stable identity between renders, otherwise edits are discarded (see [6.8](#68-editing)).
1019
- - `onRowClick` also fires for clicks inside a cell that is being edited.
1020
- - A `valueParser` that returns `NaN` stores `NaN`. Add `validate: (v) => Number.isFinite(v)` to reject it.
1021
- - Date values display with `toLocaleDateString()`. Editing a `Date` cell shows its `String()` form, so store dates as ISO strings (`"2026-11-05"`) or provide a `cellEditor`.
1478
+ ```tsx
1479
+ import { useSearchParams } from "react-router-dom";
1022
1480
 
1023
- ---
1481
+ function PeopleGrid() {
1482
+ const [params, setParams] = useSearchParams();
1483
+ const page = Number(params.get("page") ?? 0);
1484
+ const search = params.get("q") ?? "";
1024
1485
 
1025
- ## 7. Troubleshooting
1486
+ const update = (changes: Record<string, string | null>) =>
1487
+ setParams((p) => {
1488
+ for (const [k, v] of Object.entries(changes)) v === null ? p.delete(k) : p.set(k, v);
1489
+ return p;
1490
+ });
1491
+
1492
+ return (
1493
+ <DataGrid
1494
+ columns={columns}
1495
+ rows={rows}
1496
+ searchable
1497
+ pagination
1498
+ page={page}
1499
+ onPageChange={(p) => update({ page: String(p) })}
1500
+ search={search}
1501
+ onSearchChange={(q) => update({ q: q || null })}
1502
+ />
1503
+ );
1504
+ }
1505
+ ```
1506
+
1507
+ **Example: buttons outside the grid**
1508
+
1509
+ ```tsx
1510
+ const [page, setPage] = useState(0);
1511
+ const [search, setSearch] = useState("");
1512
+
1513
+ <button onClick={() => setPage(2)}>Go to page 3</button>
1514
+ <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search outside the grid" />
1515
+ <DataGrid searchable pagination pageSize={5} page={page} onPageChange={setPage} search={search} onSearchChange={setSearch} ... />
1516
+ ```
1517
+
1518
+ **Example: saved views.** Remember a user's filters and sort between visits:
1519
+
1520
+ ```tsx
1521
+ const [filters, setFilters] = useState<Record<string, FilterValue>>(() => JSON.parse(localStorage.getItem("filters") ?? "{}"));
1522
+ const [sort, setSort] = useState<SortModel[]>(() => JSON.parse(localStorage.getItem("sort") ?? "[]"));
1523
+
1524
+ useEffect(() => localStorage.setItem("filters", JSON.stringify(filters)), [filters]);
1525
+ useEffect(() => localStorage.setItem("sort", JSON.stringify(sort)), [sort]);
1526
+
1527
+ <DataGrid filters={filters} onFiltersChange={setFilters} sort={sort} onSortChange={setSort} ... />
1528
+ ```
1529
+
1530
+ ### 6.15 Server-side mode
1531
+
1532
+ When your data lives on a server, switch on `serverSide`. The grid then **does not sort, filter, search or page the rows itself**. It shows the `rows` you give it (the current page) and tells you what to fetch.
1533
+
1534
+ ```tsx
1535
+ <DataGrid
1536
+ serverSide
1537
+ columns={columns}
1538
+ rows={rows} // only the current page
1539
+ rowCount={total} // total rows on the server, for "1–10 of 237" and the page count
1540
+ pagination
1541
+ searchable
1542
+ onQueryChange={load}
1543
+ />
1544
+ ```
1545
+
1546
+ `onQueryChange(query)` is called **on mount and whenever the query changes**: the sort, the search text (after `queryDebounce` ms), a filter (after `filterDebounce` ms when typed), the page or the page size. The query is:
1547
+
1548
+ ```ts
1549
+ interface GridQuery {
1550
+ sort: SortModel[]; // [{ field: "salary", direction: "desc" }]
1551
+ filters: Record<string, FilterValue>; // only active ones, by field
1552
+ search: string; // trimmed
1553
+ page: number; // zero-based
1554
+ pageSize: number;
1555
+ }
1556
+ ```
1557
+
1558
+ Filter values have the shapes from [6.5](#65-searching-and-filtering): a string for text / exact, `{ min, max }` for number, `{ from, to }` for date, an array for select. Only columns marked `sortable` appear in `sort`.
1559
+
1560
+ **Loading is caught from a function.** If `onQueryChange` returns a promise, the grid shows the [loading skeleton](#616-loading-skeleton) until the promise settles. If it rejects, the grid shows `errorMessage` with a **Retry** button that runs the same query again. You do not manage any `loading` state.
1561
+
1562
+ ```tsx
1563
+ function PeopleGrid() {
1564
+ const [rows, setRows] = useState<Person[]>([]);
1565
+ const [total, setTotal] = useState(0);
1566
+
1567
+ const load = useCallback(async (q: GridQuery) => {
1568
+ const res = await fetch("/api/people?" + new URLSearchParams({
1569
+ page: String(q.page),
1570
+ pageSize: String(q.pageSize),
1571
+ search: q.search,
1572
+ sort: JSON.stringify(q.sort),
1573
+ filters: JSON.stringify(q.filters),
1574
+ }));
1575
+ if (!res.ok) throw new Error("Request failed"); // -> error row with Retry
1576
+ const body = await res.json();
1577
+ setRows(body.rows);
1578
+ setTotal(body.total);
1579
+ }, []);
1580
+
1581
+ return (
1582
+ <DataGrid
1583
+ serverSide
1584
+ columns={columns}
1585
+ rows={rows}
1586
+ rowCount={total}
1587
+ getRowId={(r) => r.id}
1588
+ pagination
1589
+ searchable
1590
+ onQueryChange={load}
1591
+ errorMessage="Could not reach the server."
1592
+ />
1593
+ );
1594
+ }
1595
+ ```
1596
+
1597
+ **Ignoring stale answers.** The grid ignores the *loading state* of an older query when a newer one has started, but it cannot know which response **you** store. Cancel old requests with `AbortController`, or compare a counter:
1598
+
1599
+ ```tsx
1600
+ const controller = useRef<AbortController>();
1601
+
1602
+ const load = useCallback(async (q: GridQuery) => {
1603
+ controller.current?.abort(); // cancel the previous request
1604
+ controller.current = new AbortController();
1605
+ const res = await fetch(url(q), { signal: controller.current.signal });
1606
+ const body = await res.json();
1607
+ setRows(body.rows);
1608
+ setTotal(body.total);
1609
+ }, []);
1610
+ ```
1611
+
1612
+ An aborted `fetch` rejects with an `AbortError`. If you do not want that to show the error row, catch it: `catch (e) { if ((e as Error).name !== "AbortError") throw e; }`.
1613
+
1614
+ **Using a data library.** Anything that returns a promise works:
1615
+
1616
+ ```tsx
1617
+ // the grid calls your function; you call the library
1618
+ const load = (q: GridQuery) => queryClient.fetchQuery({ queryKey: ["people", q], queryFn: () => api.people(q) })
1619
+ .then((res) => { setRows(res.rows); setTotal(res.total); });
1620
+ ```
1621
+
1622
+ **Forcing the loading state.** If your loading flag lives somewhere else (a hook, a store), pass `loading` and it replaces the promise-based state:
1623
+
1624
+ ```tsx
1625
+ const { data, isFetching } = useQuery(...);
1626
+ <DataGrid serverSide rows={data?.rows ?? []} rowCount={data?.total} loading={isFetching} onQueryChange={setQuery} />
1627
+ ```
1628
+
1629
+ **Refreshing** after your data changed (for example after deleting a row) runs the last query again:
1630
+
1631
+ ```tsx
1632
+ api.current?.refresh();
1633
+ ```
1634
+
1635
+ **Debounce.** Typing in the search box waits `queryDebounce` milliseconds (250 by default) after the last keystroke, and typing in a column filter waits `filterDebounce` (300 by default, also settable per column), so fast typing makes one request, not one per letter. Set either to `0` for instant queries. Select checkboxes, date pickers, sorting and paging query immediately.
1636
+
1637
+ **Server mode and the other features.**
1638
+
1639
+ - Selection, [CSV export of selected rows](#619-csv-export-and-the-api) and keyboard navigation work on the loaded page.
1640
+ - With `scope: "filtered"` the CSV contains the rows currently loaded (the current page), because the grid does not have the rest.
1641
+ - `defaultSort` is sent in the first query. `multiSort` lets users send several sort columns.
1642
+ - `onQueryChange` is the only place the grid asks for data. Without `serverSide`, the grid never calls it.
1643
+
1644
+ ### 6.16 Loading skeleton
1645
+
1646
+ While loading, the body shows **placeholder rows** with shimmering bars instead of data. The header, toolbar and footer stay in place, and the grid root gets `aria-busy="true"`. When the promise from `onQueryChange` settles (or `loading` turns `false`), the real rows appear.
1647
+
1648
+ ```tsx
1649
+ <DataGrid serverSide onQueryChange={load} loadingRows={8} ... />
1650
+ ```
1651
+
1652
+ - `loadingRows` is how many placeholder rows to draw. Default: the page size (at most 20), or 5 without pagination.
1653
+ - With `rowSelection` the checkbox column shows a small bar too.
1654
+ - Bar widths vary slightly from row to row so the placeholder does not look like identical stripes.
1655
+ - Colours come from `--rg-skeleton-a` and `--rg-skeleton-b`. Each theme sets them. The shimmer stops when the user has "reduce motion" turned on.
1656
+ - Style the bars with `classNames.skeleton` / `styles.skeleton` (they are `<span class="rg-grid__skeleton">`).
1657
+
1658
+ **A custom skeleton for each row.** Pass a component as `skeletonRow`. It is drawn once per placeholder row, inside a single cell that spans the whole row, and receives `{ rowIndex, columns }`:
1659
+
1660
+ ```tsx
1661
+ function PersonSkeleton({ rowIndex }: SkeletonRowProps<Person>) {
1662
+ return (
1663
+ <div style={{ display: "flex", gap: 12, alignItems: "center", padding: "4px 0" }}>
1664
+ <span className="avatar-skeleton" />
1665
+ <span style={{ display: "grid", gap: 6, flex: 1 }}>
1666
+ <span className="line-skeleton" style={{ width: `${40 + ((rowIndex * 17) % 30)}%` }} />
1667
+ <span className="line-skeleton" style={{ width: "25%" }} />
1668
+ </span>
1669
+ </div>
1670
+ );
1671
+ }
1672
+
1673
+ <DataGrid serverSide onQueryChange={load} skeletonRow={PersonSkeleton} loadingRows={6} ... />
1674
+ ```
1675
+
1676
+ You can reuse the built-in shimmer in your own placeholder by giving your elements the class `rg-grid__skeleton`:
1677
+
1678
+ ```tsx
1679
+ function RowSkeleton({ columns }: SkeletonRowProps<Person>) {
1680
+ return (
1681
+ <div style={{ display: "grid", gridTemplateColumns: `repeat(${columns.length}, 1fr)`, gap: 16 }}>
1682
+ {columns.map((c) => <span key={c.field} className="rg-grid__skeleton" style={{ width: "70%" }} />)}
1683
+ </div>
1684
+ );
1685
+ }
1686
+ ```
1687
+
1688
+ Define the skeleton component at module level (not inline in render), like every other custom component.
1689
+
1690
+ **Showing it without a server.** `loading` works in a normal grid too, for example while a parent fetches `rows`:
1691
+
1692
+ ```tsx
1693
+ <DataGrid columns={columns} rows={rows} loading={isLoading} />
1694
+ ```
1695
+
1696
+ **The error state.** If the promise from `onQueryChange` rejects, the body shows `errorMessage` (default "Could not load the data.") and a Retry button. Style it with `.rg-grid__error`:
1697
+
1698
+ ```tsx
1699
+ <DataGrid serverSide onQueryChange={load} errorMessage={<strong>The server is not answering.</strong>} ... />
1700
+ ```
1701
+
1702
+ ### 6.17 Resize, pin and hide columns
1703
+
1704
+ All three are opt-in. **There is no column reordering.**
1705
+
1706
+ **Resizing.** Add `resizable: true` to a column. A drag handle appears on its right edge. Drag it, or use a touch or pen: it uses pointer events.
1707
+
1708
+ ```tsx
1709
+ const columns: ColumnDef<Employee>[] = [
1710
+ { field: "name", resizable: true, minWidth: 100 },
1711
+ { field: "email", resizable: true, minWidth: 140, maxWidth: 480 },
1712
+ { field: "id", width: 70 }, // no handle: fixed
1713
+ ];
1714
+ ```
1715
+
1716
+ - On the first drag the grid fixes every column at the width it has at that moment (so nothing jumps) and switches the table to `table-layout: fixed`.
1717
+ - `minWidth` (at least 40px) and `maxWidth` limit the drag.
1718
+ - Widths live inside the grid and are not saved. To remember them, store the `width` you want per column yourself.
1719
+
1720
+ **Pinning.** Add `pinned: "left"` or `"right"` to keep a column visible while the rest scrolls sideways. Pinned columns are moved to the edges (left-pinned first, right-pinned last), keep their order among themselves, and have an opaque background.
1721
+
1722
+ ```tsx
1723
+ const columns: ColumnDef<Employee>[] = [
1724
+ { field: "id", width: 70, pinned: "left" },
1725
+ { field: "name", width: 160, pinned: "left" }, // sits next to ID
1726
+ { field: "email", width: 280 },
1727
+ { field: "department", width: 200 },
1728
+ { field: "salary", width: 160 },
1729
+ { field: "status", width: 120, pinned: "right" }, // always at the right edge
1730
+ ];
1731
+
1732
+ <DataGrid columns={columns} rows={rows} /> // scroll sideways: ID, Name and Status stay put
1733
+ ```
1734
+
1735
+ The row-selection checkbox column is pinned too when any column is pinned left. A thin shadow line (`.rg-grid__pinned--edge`) marks where pinned columns end. Give pinned columns an explicit `width` for the cleanest look. Pinning needs the grid to be narrower than its columns, which happens when the columns' widths add up to more than the container.
1736
+
1737
+ Colours: `--rg-pinned-bg` and `--rg-pinned-header-bg`. Stripes, hover and selection are layered on top.
1738
+
1739
+ **Showing and hiding.** Switch on `columnMenu` for a "Columns" button with a checkbox per column:
1740
+
1741
+ ```tsx
1742
+ <DataGrid
1743
+ columnMenu
1744
+ onColumnVisibilityChange={(hiddenFields) => localStorage.setItem("hidden", JSON.stringify(hiddenFields))}
1745
+ columns={[
1746
+ { field: "id", hideable: false }, // always visible, not listed
1747
+ { field: "name" },
1748
+ { field: "email" },
1749
+ { field: "notes", hidden: true }, // starts hidden
1750
+ ]}
1751
+ rows={rows}
1752
+ />
1753
+ ```
1754
+
1755
+ - `hidden: true` starts a column hidden. The menu shows it unchecked.
1756
+ - `hideable: false` keeps a column out of the menu, so it cannot be hidden.
1757
+ - The last visible column cannot be hidden.
1758
+ - Hidden columns are skipped by search, keyboard navigation and CSV export (unless `includeHidden`).
1759
+
1760
+ ### 6.18 Keyboard
1761
+
1762
+ Switch on `keyboardNavigation` to move between cells with the keyboard. The grid has **one tab stop**: Tab enters the body at the last cell you used (or the first one), and the arrow keys move from there. Tab again leaves the grid.
1763
+
1764
+ | Key (on a cell) | Action |
1765
+ | --- | --- |
1766
+ | Arrow keys | Move one cell |
1767
+ | Home / End | First / last column of the row |
1768
+ | Ctrl+Home / Ctrl+End | First cell of the first row / last cell of the last row |
1769
+ | Page Up / Page Down | Move 10 rows |
1770
+ | Enter / F2 | Start editing (editable cells) |
1771
+ | Any character | Start editing and replace the value (text and number cells) |
1772
+
1773
+ Other keys, always available:
1774
+
1775
+ | Where | Key | Action |
1776
+ | --- | --- | --- |
1777
+ | Header | Enter / Space | Sort by this column (sortable columns) |
1778
+ | Editor | Enter | Save, edit the cell below |
1779
+ | Editor | Shift+Enter | Save, edit the cell above |
1780
+ | Editor | Tab / Shift+Tab | Save, edit the next / previous cell |
1781
+ | Editor | Esc | Cancel |
1782
+ | Filter popup | Esc | Close |
1783
+ | Text filter box | Enter | Close the popup |
1784
+
1785
+ With `virtualize`, moving past the visible window scrolls the target row into view before focusing it, so you can arrow through 20,000 rows. Combined with `maxHeight`, the focused cell is scrolled into view.
1786
+
1787
+ ```tsx
1788
+ <DataGrid keyboardNavigation editable maxHeight={420} columns={columns} rows={rows} />
1789
+ ```
1790
+
1791
+ Without `keyboardNavigation`, cells are reachable with Tab (editable cells) or the mouse, as before.
1792
+
1793
+ ### 6.19 CSV export and the API
1794
+
1795
+ **The export button.** Switch on `exportable` for an "Export CSV" button in the toolbar. It downloads what the user sees: the visible columns, in order, for every row matching the search and filters (across all pages).
1796
+
1797
+ ```tsx
1798
+ <DataGrid exportable ... /> // downloads export.csv
1799
+ <DataGrid exportable={{ filename: "employees" }} ... /> // employees.csv
1800
+ <DataGrid exportable={{ filename: "page-1", scope: "page" }} ... /> // only the current page
1801
+ <DataGrid exportable={{ scope: "selected" }} rowSelection ... /> // only the selected rows
1802
+ ```
1803
+
1804
+ | `ExportOptions` | Default | Meaning |
1805
+ | --- | --- | --- |
1806
+ | `filename` | `"export"` | File name without `.csv` |
1807
+ | `scope` | `"filtered"` | `"filtered"` all rows matching search and filters, `"page"` the current page, `"selected"` the selected rows |
1808
+ | `includeHidden` | `false` | Also write hidden columns |
1809
+ | `sanitize` | `true` | Put an apostrophe in front of text starting with `=`, `+`, `-` or `@`, so Excel does not run it as a formula |
1810
+
1811
+ **What goes in the file.** The header row uses the column header names. Cell values are the displayed text (the result of `valueFormatter`, `Yes` / `No` for booleans), except that dates are written as `YYYY-MM-DD`. Numbers therefore export formatted (`$52,000`) unless you give an `exportValue`. To decide a column's value yourself, add `exportValue`:
1812
+
1813
+ ```tsx
1814
+ {
1815
+ field: "status",
1816
+ exportValue: (value) => String(value).toUpperCase(), // ACTIVE
1817
+ }
1818
+ {
1819
+ field: "salary",
1820
+ valueFormatter: (v) => `$${v.toLocaleString()}`, // shown as $52,000
1821
+ exportValue: (v) => v, // exported as 52000, so a spreadsheet can add it up
1822
+ }
1823
+ ```
1824
+
1825
+ The file starts with a byte-order mark so Excel reads accents and non-Latin text correctly. Commas, quotes and line breaks inside values are quoted.
1826
+
1827
+ **The API.** Pass a ref as `apiRef` to call methods from outside the grid:
1828
+
1829
+ ```tsx
1830
+ const api = useRef<DataGridApi<Employee> | null>(null);
1831
+
1832
+ <DataGrid apiRef={api} rowSelection ... />
1833
+
1834
+ api.current?.exportCsv({ filename: "selected", scope: "selected" }); // download
1835
+ const text = api.current?.getCsv(); // the CSV as a string
1836
+ ```
1837
+
1838
+ | Method | Does |
1839
+ | --- | --- |
1840
+ | `getCsv(options?)` | Returns the CSV text without downloading it |
1841
+ | `exportCsv(options?)` | Downloads a CSV file |
1842
+ | `getSelectedRows()` | The selected row objects |
1843
+ | `getSelectedRowIds()` | The selected ids |
1844
+ | `selectAll()` | Selects every selectable row matching the search and filters |
1845
+ | `clearSelection()` | Clears the selection |
1846
+ | `refresh()` | Runs the last query again (`serverSide` only) |
1847
+
1848
+ **Sending the CSV somewhere else** (upload, email) instead of downloading:
1849
+
1850
+ ```tsx
1851
+ const csv = api.current!.getCsv({ scope: "selected" });
1852
+ await fetch("/api/reports", { method: "POST", headers: { "Content-Type": "text/csv" }, body: csv });
1853
+ ```
1854
+
1855
+ **Your own button** next to the built-in ones:
1856
+
1857
+ ```tsx
1858
+ <DataGrid
1859
+ apiRef={api}
1860
+ toolbar={<button className="rg-grid__tool-btn" onClick={() => api.current?.exportCsv({ filename: "all", includeHidden: true })}>Export everything</button>}
1861
+ ...
1862
+ />
1863
+ ```
1864
+
1865
+ ### 6.20 Virtualization
1866
+
1867
+ For tens of thousands of rows, switch on `virtualize`. Only the rows in (and just around) the scroll window are in the DOM; empty spacer rows stand in for the rest, so the scroll bar still reflects the full data.
1868
+
1869
+ ```tsx
1870
+ <DataGrid
1871
+ virtualize
1872
+ rowHeight={40} // every row must be exactly this tall
1873
+ maxHeight={480} // the grid scrolls inside this height (400 if you give none)
1874
+ overscan={8} // extra rows drawn above and below
1875
+ columns={columns}
1876
+ rows={twentyThousandRows}
1877
+ getRowId={(r) => r.id}
1878
+ searchable
1879
+ />
1880
+ ```
1881
+
1882
+ - **Fixed row height.** Rows are laid out as `rowHeight` pixels each and cell text does not wrap (long text is cut off). Set `rowHeight` to fit your content.
1883
+ - Use it without `pagination` so everything scrolls in one list. Sorting, search and filters still work on the whole data set.
1884
+ - Row animation is skipped while virtualizing, because rows come and go as you scroll.
1885
+ - Keyboard navigation, selection and editing all work. Moving focus to a row outside the window scrolls it into view first.
1886
+ - Choose a `rowHeight` that suits your padding: 40 is right for the default cell padding.
1887
+
1888
+ ```tsx
1889
+ // 50,000 rows, searchable, selectable, keyboard friendly
1890
+ <DataGrid virtualize rowHeight={36} maxHeight="70vh" searchable rowSelection keyboardNavigation theme="graphite" ... />
1891
+ ```
1892
+
1893
+ ### 6.21 Accessibility
1894
+
1895
+ - The table has `role="grid"`. Sortable headers are focusable, expose `aria-sort`, and work with Enter and Space. Columns that are not sortable are not focus stops.
1896
+ - Editable cells are focusable (`tabindex="0"`). With `keyboardNavigation` the body has a single tab stop and arrow keys move between cells ([6.18](#618-keyboard)).
1897
+ - The search box, filter indicators (`aria-haspopup="dialog"`, `aria-expanded`), filter popups (`role="dialog"`), the Columns menu, editors, page buttons and page-size menu have `aria-label`s. Filter inputs are named after their column ("Minimum Budget", "Due from"). Checkboxes in columns with an empty header are named after the field.
1898
+ - Selected rows have `aria-selected="true"`. Row checkboxes are named "Select row N", and the header one "Select all rows".
1899
+ - The "N selected" count is a status region (`role="status"`).
1900
+ - While loading, the grid has `aria-busy="true"` and the placeholder rows are hidden from assistive technology. The error message has `role="alert"`.
1901
+ - Invalid editors set `aria-invalid="true"`.
1902
+ - The shimmer and row animations stop when the system has "reduce motion" turned on.
1903
+ - Custom components you supply (renderers, editors, skeleton rows, indicators) are responsible for their own accessibility.
1904
+
1905
+ ### 6.22 TypeScript
1906
+
1907
+ All types are exported:
1908
+
1909
+ ```ts
1910
+ import type {
1911
+ // grid and columns
1912
+ DataGridProps, ColumnDef, DataGridApi, GridTheme, GridSlot, FieldPath,
1913
+ // cells, editors, headers
1914
+ CellParams, CellEditEvent, CellEditorProps, CellEditableParams, HeaderRendererProps, MoveDirection,
1915
+ // sorting, paging
1916
+ SortModel, SortDirection, PaginationPosition,
1917
+ // filters
1918
+ FilterType, FilterValue, FilterOption, NumberRangeFilter, DateRangeFilter, FilterRendererProps, FilterIndicatorProps,
1919
+ // selection, server, export, loading
1920
+ RowSelection, GridQuery, ExportOptions, SkeletonRowProps,
1921
+ // checkbox
1922
+ ColumnCheckboxOptions, CheckboxProps, CheckboxVariant, CheckboxSlot,
1923
+ } from "react-gridix";
1924
+ ```
1925
+
1926
+ Typing tips:
1927
+
1928
+ ```tsx
1929
+ const columns: ColumnDef<Employee>[] = [ ... ]; // field is checked against Employee (dot paths too)
1930
+ function Cell({ row }: CellParams<Employee>) { ... } // row is typed as Employee
1931
+
1932
+ const api = useRef<DataGridApi<Employee> | null>(null); // typed apiRef
1933
+ const [sel, setSel] = useState<RowSelection<Employee>>({ ids: [], rows: [] });
1934
+ const [filters, setFilters] = useState<Record<string, FilterValue>>({});
1935
+
1936
+ // narrowing a filter value yourself (for example when building a server query)
1937
+ function toParams(f: Record<string, FilterValue>) {
1938
+ return Object.entries(f).map(([field, v]) => {
1939
+ if (typeof v === "string") return `${field}~${v}`; // text / exact
1940
+ if (Array.isArray(v)) return `${field}=${v.join(",")}`; // select
1941
+ if ("min" in v || "max" in v) return `${field}:${(v as NumberRangeFilter).min ?? ""}..${(v as NumberRangeFilter).max ?? ""}`;
1942
+ return `${field}:${(v as DateRangeFilter).from ?? ""}..${(v as DateRangeFilter).to ?? ""}`; // date
1943
+ });
1944
+ }
1945
+ ```
1946
+
1947
+ ### 6.23 Recipes
1948
+
1949
+ **Read-only grid**
1950
+
1951
+ ```tsx
1952
+ <DataGrid columns={columns} rows={rows} /> // editable, sortable, searchable... all default to off
1953
+ ```
1954
+
1955
+ **A typical admin table**
1956
+
1957
+ ```tsx
1958
+ <DataGrid
1959
+ columns={columns} // sortable / filterable chosen per column
1960
+ rows={rows}
1961
+ getRowId={(r) => r.id}
1962
+ searchable
1963
+ pagination
1964
+ rowSelection
1965
+ columnMenu
1966
+ exportable={{ filename: "users" }}
1967
+ striped
1968
+ theme="lavender"
1969
+ />
1970
+ ```
1971
+
1972
+ **Currency column that accepts "$1,200" when typed**
1973
+
1974
+ ```tsx
1975
+ {
1976
+ field: "salary",
1977
+ type: "number",
1978
+ align: "right",
1979
+ sortable: true,
1980
+ valueFormatter: (v) => (v == null ? "" : money.format(v)),
1981
+ valueParser: (text) => Number(text.replace(/[$,]/g, "")),
1982
+ validate: (v) => Number.isFinite(v),
1983
+ }
1984
+ ```
1985
+
1986
+ (The editor opens with the raw number, such as `52000`. The parser only matters if the user types symbols.)
1987
+
1988
+ **Only one column editable**
1989
+
1990
+ ```tsx
1991
+ <DataGrid columns={[{ field: "id" }, { field: "notes", editable: true }]} />
1992
+ ```
1993
+
1994
+ **Lock rows after approval**
1995
+
1996
+ ```tsx
1997
+ { field: "amount", editable: ({ row }) => !row.approved }
1998
+ ```
1999
+
2000
+ **Scrolling grid with sticky header**
2001
+
2002
+ ```tsx
2003
+ <DataGrid maxHeight="60vh" ... />
2004
+ ```
2005
+
2006
+ **Highlight a row based on its data**
2007
+
2008
+ ```tsx
2009
+ <DataGrid rowClassName={(row) => (row.overdue ? "bg-red-50" : "")} />
2010
+ ```
2011
+
2012
+ **Overdue dates in red**
2013
+
2014
+ ```tsx
2015
+ {
2016
+ field: "due",
2017
+ type: "date",
2018
+ cellStyle: ({ value }) => (value && String(value) < new Date().toISOString().slice(0, 10) ? { color: "#dc2626" } : {}),
2019
+ }
2020
+ ```
2021
+
2022
+ **Status as a coloured badge, with an exact filter**
2023
+
2024
+ ```tsx
2025
+ const Badge = ({ value }: CellParams<Task>) => <span className={`badge badge-${value}`}>{String(value)}</span>;
2026
+
2027
+ { field: "status", sortable: true, filterable: true, filterType: "select", cellRenderer: Badge }
2028
+ ```
2029
+
2030
+ **Delete the selected rows**
2031
+
2032
+ ```tsx
2033
+ const [rows, setRows] = useState(initial);
2034
+ const [ids, setIds] = useState<(string | number)[]>([]);
2035
+
2036
+ <button disabled={!ids.length} onClick={() => { setRows((r) => r.filter((x) => !ids.includes(x.id))); setIds([]); }}>
2037
+ Delete {ids.length}
2038
+ </button>
2039
+ <DataGrid rows={rows} columns={columns} getRowId={(r) => r.id} rowSelection selectedRowIds={ids} onSelectionChange={(s) => setIds(s.ids)} />
2040
+ ```
2041
+
2042
+ **Bulk edit the selected rows** (`onRowsChange` gives the whole array back):
2043
+
2044
+ ```tsx
2045
+ const markDone = () => setRows((rs) => rs.map((r) => (ids.includes(r.id) ? { ...r, status: "done" } : r)));
2046
+ ```
2047
+
2048
+ **Choose the page size once and remember it**
2049
+
2050
+ ```tsx
2051
+ <DataGrid pagination pageSize={Number(localStorage.getItem("ps")) || 25} onPageSizeChange={(n) => localStorage.setItem("ps", String(n))} />
2052
+ ```
2053
+
2054
+ **A fast, big, read-only table**
2055
+
2056
+ ```tsx
2057
+ <DataGrid virtualize rowHeight={32} maxHeight="80vh" animateRows={false} searchable columns={columns} rows={hugeArray} getRowId={(r) => r.id} />
2058
+ ```
2059
+
2060
+ **An infinite-looking server table with a dark theme**
2061
+
2062
+ ```tsx
2063
+ <DataGrid serverSide pagination searchable multiSort theme="aurora" columns={columns} rows={rows} rowCount={total} onQueryChange={load} />
2064
+ ```
2065
+
2066
+ ### 6.24 Known limitations
2067
+
2068
+ Not included:
2069
+
2070
+ - **Column reordering** (by design)
2071
+ - Row grouping, tree data, row detail panels and column spanning
2072
+ - Infinite scroll in server mode (paging is used instead)
2073
+ - Saving column widths or visibility between visits (use `onColumnVisibilityChange` and your own storage)
2074
+ - Variable row heights while virtualizing
2075
+
2076
+ Behaviours to be aware of:
2077
+
2078
+ - Text filters match by substring (`active` also matches `inactive`). Use `filterType: "exact"` or `"select"` for exact choices.
2079
+ - Only one filter popup is open at a time.
2080
+ - Stripes and the row animation restart on every page, sort, search or filter change.
2081
+ - `pageSize` is read once, on mount (use `onPageSizeChange`). `defaultSort` and `defaultSelectedRowIds` are read once too: to change them later, control the state.
2082
+ - `rows` must keep a stable identity between renders, otherwise edits are discarded (see [6.9](#69-editing)).
2083
+ - `onRowClick` also fires for clicks inside a cell that is being edited.
2084
+ - A `valueParser` that returns `NaN` stores `NaN`. Add `validate: (v) => Number.isFinite(v)` to reject it.
2085
+ - `virtualize` needs fixed-height rows. A cell with wrapped text will be cut off.
2086
+ - Pinned columns only stick when the table is wider than its container.
2087
+ - In `serverSide` mode the grid cannot export or select rows it has never loaded.
2088
+ - The date calendar starts weeks on Sunday and has no week numbers or time of day.
2089
+
2090
+ ---
2091
+
2092
+ ## 7. Demos
2093
+
2094
+ The repository contains a playground with one page per feature:
2095
+
2096
+ | Demo (`#id`) | Shows |
2097
+ | --- | --- |
2098
+ | Basic (`#basic`) | Opt-in sorting, global search, per-column filter dots with popups, pagination, controlled `SortModel[]` |
2099
+ | Editing (`#editing`) | Enter-moves-down editing, Tab, validation, locked cells, `onCellEdit` |
2100
+ | Custom components (`#custom`) | `cellRenderer`, `cellEditor`, `headerRenderer`, a custom funnel `filterIndicator`, a per-column indicator style, a `filterRenderer` in the popup |
2101
+ | Checkboxes (`#checkboxes`) | Boolean columns with the three variants, an empty-label header, a custom `★` character, select-all headers, locking, styling from outside, and a standalone `Checkbox` |
2102
+ | Styling (`#styling`) | Tailwind classes and inline styles, column `className` / `style`, and `stripeColors` |
2103
+ | Pagination (`#pagination`) | Top, bottom and both, no pagination, the row animation toggle with two **custom animation classes**, a **row-number (sequence) column**, and a **controlled page and search** grid driven by outside buttons |
2104
+ | Nested fields (`#nested`) | `contact.phone` style dot paths: editing, search, filter, sort, and proof the original data is not mutated |
2105
+ | Row selection (`#selection`) | Multiple and single modes, controlled ids, `isRowSelectable`, Shift+click ranges, select-all across pages, `apiRef.selectAll()`, a Delete button |
2106
+ | Server-side (`#server`) | A fake 237-row database with 700 ms latency: server sorting, multi-sort, search debounce, filters, paging, the loading skeleton (default and custom row), and a switch that makes the request fail so you can see the error and Retry |
2107
+ | Columns (`#columns`) | Resizing, pinned columns on both sides, the Columns menu, arrow-key navigation, CSV export with `exportValue`, multi-column sort |
2108
+ | Large data (`#large`) | 20,000 virtualized rows with search, sorting and a theme |
2109
+ | Dates and filters (`#dates`) | `type: "date"` with the calendar editor and range-calendar filter, `dateMin` / `dateMax`, a `Date`-object column, from/to, number range, select and exact filters |
2110
+ | Themes (`#themes`) | A switcher for none and all 8 themes |
2111
+
2112
+ Run it:
2113
+
2114
+ ```bash
2115
+ npm install
2116
+ npm run dev
2117
+ ```
2118
+
2119
+ Open the URL it prints (usually `http://localhost:5173`). Link straight to a demo with the `#id` in the table. The source for each demo is in `demo/examples/`, written as small, commented, copy-paste-able files.
2120
+
2121
+ The Styling demo loads Tailwind from its CDN, so it needs an internet connection. The rest works offline.
2122
+
2123
+ ---
2124
+
2125
+ ## 8. Development, building and publishing
2126
+
2127
+ ### Project layout
2128
+
2129
+ ```
2130
+ src/
2131
+ index.ts Public exports
2132
+ components/Checkbox/ Checkbox.tsx, checkbox.css
2133
+ components/DataGrid/
2134
+ DataGrid.tsx The grid (state, rendering, keyboard, selection, server mode, virtualization)
2135
+ types.ts All public types, with doc comments
2136
+ ColumnFilter.tsx The filter indicator and its popup (text, number, date, select)
2137
+ ColumnMenu.tsx The "Columns" show / hide menu
2138
+ Popup.tsx Portal popup used by the two above
2139
+ filters.ts Filter kinds, matching and select options
2140
+ dates.ts Date parsing and formatting helpers
2141
+ csv.ts CSV building and download
2142
+ hooks.ts useControlled and a layout-effect helper
2143
+ paths.ts Dot notation: FieldPath, getByPath, setByPath
2144
+ utils.ts Small helpers
2145
+ datagrid.css Layout and component styles (zero specificity)
2146
+ themes.css The 8 themes
2147
+ demo/ Playground (not published)
2148
+ examples/ One file per demo page
2149
+ vite.config.ts Library build
2150
+ vite.demo.config.ts Playground dev server
2151
+ ```
2152
+
2153
+ ### Scripts
2154
+
2155
+ | Command | Does |
2156
+ | --- | --- |
2157
+ | `npm run dev` | Starts the demo playground using the library source |
2158
+ | `npm run typecheck` | Runs the TypeScript compiler without emitting files |
2159
+ | `npm run build` | Builds `dist/`: ES module, CommonJS, bundled type definitions and `style.css` |
2160
+
2161
+ ### Testing the built package in another app
2162
+
2163
+ ```bash
2164
+ npm run build
2165
+ npm pack # creates react-gridix-0.1.0.tgz
2166
+ # in another project:
2167
+ npm install /path/to/react-gridix-0.1.0.tgz
2168
+ ```
2169
+
2170
+ ### Publishing to npm
2171
+
2172
+ 1. Create an npm account, enable two-factor authentication, and run `npm login`.
2173
+ 2. Check the name is free with `npm view react-gridix`. If taken, rename the package, for example `@yourname/react-gridix` (scoped packages need `npm publish --access public`).
2174
+ 3. In `package.json` fill in `author`, `repository`, `homepage` and `bugs`, and put your name in the `LICENSE` file (it says `YOUR NAME`).
2175
+ 4. Run `npm run typecheck` and `npm run build`. `prepublishOnly` runs both automatically on `npm publish`. Confirm `dist/` contains `react-gridix.js`, `react-gridix.cjs`, `index.d.ts` and `style.css`.
2176
+ 5. Run `npm publish --dry-run` and check that only `dist/`, `package.json`, `README.md` and `LICENSE` are listed.
2177
+ 6. Run `npm publish`.
2178
+ 7. For later releases bump the version first (`npm version patch`, `minor` or `major`). npm will not accept the same version twice. This release changes defaults (see [section 10](#10-migrating-from-the-earlier-version)), so a `minor` bump while below 1.0 (or `major` after) is appropriate.
2179
+
2180
+ ---
2181
+
2182
+ ## 9. Troubleshooting
1026
2183
 
1027
2184
  | Problem | Cause and fix |
1028
2185
  | --- | --- |
1029
2186
  | Everything looks unstyled | Import `react-gridix/style.css` once in your app |
1030
- | `theme="ocean"` does nothing | The theme CSS ships in `style.css`: make sure it is imported. The attribute `data-rk-theme` should appear on the grid root |
1031
- | I can't find the filter input | Filters are per column: set `filterable: true` in the column definition, then click the dot in the header. The old grid-level `columnFilters` prop no longer exists |
2187
+ | `theme="ocean"` does nothing | The theme CSS ships in `style.css`: make sure it is imported. The attribute `data-rg-theme` should appear on the grid root |
2188
+ | Headers don't sort | Sorting is per column: add `sortable: true` to the column definition. There is no grid-level `sortable` prop any more |
2189
+ | There is no search box | Pass `searchable` |
2190
+ | There is no page info or page buttons | Pass `pagination`. Without it every row is shown and there is no footer |
2191
+ | I can't find the filter input | Filters are per column: set `filterable: true`, then click the dot in the header |
2192
+ | A date column shows `2026-11-05` instead of `Nov 5, 2026` | The column needs `type: "date"` |
2193
+ | The date calendar never opens | The cell must be editable (`editable` on the grid or the column), then double-click it or press Enter / F2 |
2194
+ | A sequence column shows nothing or a type error | Give it `type: "sequence"` and a `field` that starts with `#`, for example `{ field: "#", type: "sequence" }` |
2195
+ | My custom row animation does nothing | `animateRows` is `false`, `virtualize` is on, or your class does not set `animation`. Check that the keyframes exist and that the class name is spelled the same in CSS |
2196
+ | The number filter popup asks for Min and Max, but I wanted text | Number columns default to a range filter. Set `filterType: "text"` |
2197
+ | A typed filter feels slow, or fires on every key | Change the wait with `filterDebounce` on the grid (default 300 ms) or on one column. Use `0` for no waiting |
2198
+ | Select filter lists the wrong labels | Pass `filterOptions` with `{ value, label }` pairs. `value` must equal what is stored in the row |
2199
+ | Selecting rows does nothing | Add `rowSelection`. Give the grid `getRowId` so selection survives sorting and paging |
2200
+ | The server grid asks for data twice on mount | In React StrictMode (development) effects run twice. The production build calls it once. Your handler should be safe to call repeatedly |
2201
+ | The server grid shows no skeleton | `onQueryChange` must **return** the promise (`return fetch(...)` or an `async` function). A function that starts a request and returns nothing cannot be tracked. Alternatively pass `loading` |
2202
+ | The server grid shows old rows after I changed a filter | You stored an older response last. Cancel the previous request with `AbortController` or compare a request counter |
2203
+ | Page 1 is shown after every search | By design: the page resets when search, filters, sort or page size change |
2204
+ | Resize handles do nothing | The column needs `resizable: true` |
2205
+ | Pinned columns scroll away | The columns must be wider than the container (give them `width`s), otherwise there is nothing to scroll. Give pinned columns an explicit `width` |
2206
+ | Virtualized rows look cut off or overlap | `rowHeight` is smaller than your content. Increase it, or shorten the content |
2207
+ | Virtualized grid is tiny | It scrolls inside `maxHeight` (400 if unset). Set `maxHeight` |
2208
+ | Arrow keys do nothing | Add `keyboardNavigation` and click or Tab into a cell first |
2209
+ | CSV opens with strange characters in Excel | The file has a BOM and is UTF-8. Use Excel's Data > From Text if your version ignores it |
2210
+ | CSV shows `$52,000` where I wanted `52000` | Add `exportValue: (v) => v` to the column |
1032
2211
  | Type error on `field: "contact.phone"` | The path must exist in your row type (optional objects are fine). If `rows` is typed loosely (`any`), type the columns as `ColumnDef<Person>[]` |
1033
2212
  | My checkbox column header is empty | By design: boolean columns default to no label. Set `headerName` to show one |
1034
- | No row animation | `animateRows` is off, or the system has "reduce motion" turned on |
2213
+ | No row animation | `animateRows` is off, `virtualize` is on, or the system has "reduce motion" turned on |
1035
2214
  | My Tailwind class doesn't change the grid | Put it in the right slot (`classNames.headerCell`, and so on). The library CSS has zero specificity, so a class in the right place wins |
1036
2215
  | Edits disappear on the next render | `rows` gets a new identity every render. Keep it in `useState` or `useMemo` |
1037
- | A custom editor or renderer loses focus or resets while typing | The component is defined inline inside render. Move it to module level |
2216
+ | A custom editor, renderer or skeleton loses focus or resets while typing | The component is defined inline inside render. Move it to module level |
1038
2217
  | Typing in a custom editor column doesn't start editing | By design: custom editors open with double-click, Enter or F2 |
1039
2218
  | Enter doesn't move to the next row | The cell below is locked, you are on the last row of the page, or `enterMovesDown` is `false` |
1040
- | Filtering "active" also shows "inactive" | Filters are substring matches. Use a `filterRenderer` with distinct values or different display text |
1041
- | Changing `pageSize` or `defaultSort` after mount does nothing | They are initial values only |
2219
+ | Changing `pageSize` or `defaultSort` after mount does nothing | They are initial values only. Control `page` / `sort` instead |
1042
2220
  | A checkbox in a boolean column is greyed out and won't toggle | The cell isn't editable. Set `editable` on the grid, or `editable: true` on that column |
1043
2221
  | Type error: `field` not assignable | `field` must be a key of your row type. Check the spelling, and that `ColumnDef<T>` uses the same `T` as `rows` |
2222
+
2223
+ ---
2224
+
2225
+ ## 10. Migrating from the earlier version
2226
+
2227
+ ### Renamed: react-gridix
2228
+
2229
+ The library is now **`react-gridix`**, and every CSS prefix changed from `rk` to `rg`.
2230
+
2231
+ | Before | Now |
2232
+ | --- | --- |
2233
+ | `npm install react-ui-kit` | `npm install react-gridix` |
2234
+ | `import ... from "react-ui-kit"` | `import ... from "react-gridix"` |
2235
+ | `import "react-ui-kit/style.css"` | `import "react-gridix/style.css"` |
2236
+ | CSS variables `--rk-header-bg`, `--rk-primary`, ... | `--rg-header-bg`, `--rg-primary`, ... |
2237
+ | Classes `.rk-grid__row`, `.rk-checkbox`, ... | `.rg-grid__row`, `.rg-checkbox`, ... |
2238
+ | Theme selector `[data-rk-theme="brand"]` | `[data-rg-theme="brand"]` |
2239
+
2240
+ Search your project's CSS for `rk-` and replace it with `rg-`. A one-line fix in a Unix shell: `sed -i 's/\brk-/rg-/g' src/**/*.css`.
2241
+
2242
+ ### Other changes
2243
+
2244
+ Also new in this release: `filterDebounce`, a smaller default checkbox (17px instead of 20px, with a tighter gap and a narrower selection column), a calendar for date cells and date filters, the `sequence` column type, `rowAnimationClassName`, and re-tuned theme colours (stronger header contrast, clearer inputs in the dark themes).
2245
+
2246
+ ### Changed defaults
2247
+
2248
+ Three defaults changed so that every feature is opt-in.
2249
+
2250
+ | Before | Now | What to do |
2251
+ | --- | --- | --- |
2252
+ | Grid prop `sortable` (default `true`), column `sortable` override | **Column** `sortable` only, default `false`. The grid-level prop is gone | Add `sortable: true` to each column that should sort. Remove `sortable` / `sortable={false}` from the grid |
2253
+ | `defaultSort` / `onSortChange` used one `SortModel` or `null` | `defaultSort` takes one `SortModel` **or an array**. `onSortChange` receives an **array** (empty when unsorted) | Change `(sort) => sort?.field` to `(sort) => sort[0]?.field` |
2254
+ | `searchable` default `true` | Default `false` | Add `searchable` to grids that should show the search box |
2255
+ | `pagination` default `true` | Default `false`. With it off there is no footer and no row count | Add `pagination` to grids that should page |
2256
+ | Filters were text only | Filter kinds: text, exact, number, date, select | Nothing to change. Number columns now get a Min / Max popup, boolean columns a checkbox list |
2257
+ | `FilterRendererProps` | Unchanged (`value: string`) | |
2258
+
2259
+ A quick way to get the old behaviour everywhere:
2260
+
2261
+ ```tsx
2262
+ // before: <DataGrid columns={columns} rows={rows} />
2263
+ const withSorting = (cols: ColumnDef<Row>[]) => cols.map((c) => ({ sortable: true, ...c }));
2264
+
2265
+ <DataGrid columns={withSorting(columns)} rows={rows} searchable pagination />
2266
+ ```
2267
+
2268
+ Everything else in this document is new and optional.