react-gridix 0.1.0 → 0.1.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 ADDED
@@ -0,0 +1,1043 @@
1
+ # react-gridix
2
+
3
+ A React `DataGrid` with per-column filters, in-cell editing, nested fields, themes and custom components, plus the `Checkbox` it uses.
4
+
5
+ **Contents**
6
+
7
+ 1. [Overview](#1-overview)
8
+ 2. [Installation](#2-installation)
9
+ 3. [Quick start](#3-quick-start)
10
+ 4. [Theming](#4-theming)
11
+ 5. [Checkbox](#5-checkbox)
12
+ 6. [DataGrid](#6-datagrid)
13
+ 7. [Demos](#7-demos)
14
+ 8. [Development, building and publishing](#8-development-building-and-publishing)
15
+ 9. [Troubleshooting](#9-troubleshooting)
16
+
17
+ ---
18
+
19
+ ## 1. Overview
20
+
21
+ | Component | Purpose |
22
+ | --- | --- |
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 |
25
+
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
+
28
+ ---
29
+
30
+ ## 2. Installation
31
+
32
+ **Requirements:** React 18 or newer and `react-dom`.
33
+
34
+ ```bash
35
+ npm install react-gridix
36
+ ```
37
+
38
+ The stylesheet is included in the package's `style.css`. Import it once, near the root of your app:
39
+
40
+ ```tsx
41
+ import "react-gridix/style.css";
42
+ ```
43
+
44
+ The library does not inject CSS by itself. If the grid looks unstyled, this import is missing.
45
+
46
+ ---
47
+
48
+ ## 3. Quick start
49
+
50
+ ```tsx
51
+ import { useState } from "react";
52
+ import { DataGrid, type ColumnDef } from "react-gridix";
53
+ import "react-gridix/style.css";
54
+
55
+ interface Person {
56
+ id: number;
57
+ name: string;
58
+ contact: { email: string; phone: string };
59
+ }
60
+
61
+ const columns: ColumnDef<Person>[] = [
62
+ { field: "id", headerName: "ID", type: "number", editable: false },
63
+ { field: "name", headerName: "Name", filterable: true },
64
+ { field: "contact.email", headerName: "Email" },
65
+ { field: "contact.phone", headerName: "Phone" },
66
+ ];
67
+
68
+ export default function App() {
69
+ 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" } },
72
+ ]);
73
+
74
+ return (
75
+ <DataGrid
76
+ columns={columns}
77
+ rows={rows}
78
+ getRowId={(r) => r.id}
79
+ editable
80
+ onRowsChange={setRows}
81
+ theme="ocean"
82
+ />
83
+ );
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## 4. Theming
90
+
91
+ ### 4.1 Built-in themes
92
+
93
+ Pass a theme name to the grid. **Without a `theme` prop nothing changes**: the grid keeps its plain default look.
94
+
95
+ ```tsx
96
+ <DataGrid theme="midnight" ... />
97
+ ```
98
+
99
+ | Theme | Look |
100
+ | --- | --- |
101
+ | `"ocean"` | Light, cool blues, blue gradient header, soft shadow |
102
+ | `"midnight"` | Dark slate, indigo accent, dark popups and inputs |
103
+ | `"sunset"` | Warm cream, orange-to-pink gradient header, serif font, rounded corners |
104
+ | `"forest"` | Calm greens, solid dark-green header, sharper corners |
105
+
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.
107
+
108
+ **Making your own theme.** Any other string works as a theme name. Define the variables you want:
109
+
110
+ ```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;
116
+ }
117
+ ```
118
+
119
+ ```tsx
120
+ <DataGrid theme="brand" ... />
121
+ ```
122
+
123
+ ### 4.2 CSS variables
124
+
125
+ 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
+
127
+ | Variable | Default | Used for |
128
+ | --- | --- | --- |
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 |
165
+
166
+ ```css
167
+ :root {
168
+ --rk-primary: #0d9488;
169
+ --rk-row-hover: #f0fdfa;
170
+ }
171
+ ```
172
+
173
+ ---
174
+
175
+ ## 5. Checkbox
176
+
177
+ A checkbox in three looks. Every variant toggles with **one click** (or the Space key when it has focus), and clicking the label text toggles it too.
178
+
179
+ ```tsx
180
+ <Checkbox variant="button" checked={a} onChange={setA} label="Button + span" />
181
+ <Checkbox variant="tick" checked={b} onChange={setB} label="Tick" />
182
+ <Checkbox variant="native" checked={c} onChange={setC} label="Native" />
183
+ ```
184
+
185
+ | Variant | What it is |
186
+ | --- | --- |
187
+ | `"button"` | A `<button role="checkbox">` drawn as a box, with a `<span>` inside that is hidden when unchecked and shown when checked |
188
+ | `"tick"` | The same button, but the box fills with the checked colour and the span holds a tick icon |
189
+ | `"native"` | The browser's own `<input type="checkbox">` |
190
+
191
+ | Prop | Type | Default | Description |
192
+ | --- | --- | --- | --- |
193
+ | `checked` | `boolean` | required | Current value (controlled) |
194
+ | `indeterminate` | `boolean` | `false` | Shows the mixed ("some selected") state: a dash instead of a tick. Use it with `checked={false}`. A click then calls `onChange(true)` |
195
+ | `onChange` | `(checked: boolean) => void` | required | Called once per toggle with the new value |
196
+ | `variant` | `"button" \| "tick" \| "native"` | `"native"` | Which look to use |
197
+ | `disabled` | `boolean` | `false` | Disables the checkbox and dims it |
198
+ | `label` | `ReactNode` | | Text next to the box. Clicking it toggles the checkbox. An empty string, `null` or `false` renders no label |
199
+ | `aria-label` | `string` | | Accessible name when there is no visible `label` |
200
+ | `id` | `string` | | `id` of the control |
201
+ | `name` | `string` | | `name` of the input (`"native"` variant only) |
202
+ | `indicator` | `ReactNode` | | A custom character or icon drawn inside the `"button"` and `"tick"` variants when checked, for example `"★"` |
203
+ | `className` | `string` | | Classes for the root element (same as `classNames.root`) |
204
+ | `style` | `CSSProperties` | | Inline styles for the root element (same as `styles.root`) |
205
+ | `classNames` | `Partial<Record<CheckboxSlot, string>>` | | Classes per part |
206
+ | `styles` | `Partial<Record<CheckboxSlot, CSSProperties>>` | | Inline styles per part |
207
+
208
+ **Parts (`CheckboxSlot`):** `root` (the wrapping `<label>`), `control` (the `<button>` or `<input>`), `indicator` (the span inside, button variants only) and `label` (the text).
209
+
210
+ **Customising the look from outside**
211
+
212
+ 1. **Classes and inline styles per part**, including Tailwind:
213
+
214
+ ```tsx
215
+ <Checkbox
216
+ variant="tick"
217
+ checked={on}
218
+ onChange={setOn}
219
+ classNames={{ control: "rounded-full border-emerald-500 aria-checked:bg-emerald-500" }}
220
+ styles={{ root: { gap: 12 } }}
221
+ />
222
+ ```
223
+
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:
226
+
227
+ ```tsx
228
+ <Checkbox styles={{ root: { "--rk-checkbox-color": "#dc2626" } as CSSProperties }} ... />
229
+ ```
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"`).
232
+
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`.
234
+
235
+ The checkbox CSS is written with `:where()` (zero specificity), so any class you add wins without `!important`.
236
+
237
+ **Accessibility:** the button variants have `role="checkbox"` and `aria-checked`, and the native variant is a real checkbox. When you don't pass a `label`, pass an `aria-label`.
238
+
239
+ ---
240
+
241
+ ---
242
+
243
+ ## 6. DataGrid
244
+
245
+ ### 6.1 Concepts
246
+
247
+ ```tsx
248
+ <DataGrid columns={columns} rows={rows} />
249
+ ```
250
+
251
+ - **`rows`** is an array of plain objects.
252
+ - **`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.
254
+ - **`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).
256
+
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)).
258
+
259
+ ### 6.2 DataGrid props
260
+
261
+ | Prop | Type | Default | Description |
262
+ | --- | --- | --- | --- |
263
+ | `columns` | `ColumnDef<T>[]` | required | Column definitions |
264
+ | `rows` | `T[]` | required | Data |
265
+ | `getRowId` | `(row, index) => string \| number` | row index | Stable id for each row |
266
+ | `editable` | `boolean` | `false` | Allow editing cells. Columns can override this |
267
+ | `enterMovesDown` | `boolean` | `true` | After Enter saves a cell, edit the cell below |
268
+ | `onCellEdit` | `(event: CellEditEvent<T>) => void` | | Called after a cell edit is saved |
269
+ | `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
+ | `onRowClick` | `(row, rowIndex) => void` | | Called when a row is clicked |
280
+ | `toolbar` | `ReactNode` | | Extra content on the right of the toolbar |
281
+ | `emptyMessage` | `ReactNode` | `"No rows to show"` | Shown when no rows match |
282
+ | `striped` | `boolean` | `false` | Alternate row shading |
283
+ | `stripeColors` | `[string, string]` | | Two colours for the alternating rows. Implies `striped`. See [striped rows](#striped-rows) |
284
+ | `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) |
287
+ | `maxHeight` | `number \| string` | | Makes the body scroll, with a sticky header. Example: `400` or `"60vh"` |
288
+ | `className` | `string` | | Classes for the root element (Tailwind works). The grid is `width: 100%` of its container by default |
289
+ | `style` | `CSSProperties` | | Inline styles for the root element |
290
+ | `classNames` | `Partial<Record<GridSlot, string>>` | | Classes for individual parts. See [6.10](#610-styling) |
291
+ | `styles` | `Partial<Record<GridSlot, CSSProperties>>` | | Inline styles for individual parts |
292
+ | `rowClassName` | `string \| (row, rowIndex) => string` | | Classes for rows |
293
+ | `rowStyle` | `CSSProperties \| (row, rowIndex) => CSSProperties` | | Inline styles for rows |
294
+
295
+ ### 6.3 Column definitions
296
+
297
+ ```ts
298
+ const columns: ColumnDef<Employee>[] = [
299
+ { field: "name", headerName: "Name" },
300
+ { field: "salary", headerName: "Salary", type: "number", align: "right" },
301
+ ];
302
+ ```
303
+
304
+ | Option | Type | Description |
305
+ | --- | --- | --- |
306
+ | `field` | `FieldPath<T>` | **Required.** Key of the row object this column reads and writes. Use dot notation for nested objects: `"contact.phone"` |
307
+ | `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"` |
309
+ | `width` | `number \| string` | Header cell width |
310
+ | `minWidth` | `number \| string` | Header cell minimum width |
311
+ | `align` | `"left" \| "center" \| "right"` | Text alignment for the header, cells and editor |
312
+ | `sortable` | `boolean` | Overrides the grid-level `sortable` |
313
+ | `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 |
315
+ | `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 |
320
+ | `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 |
322
+ | `valueParser` | `(text, row) => unknown` | Turns typed text into the value to store |
323
+ | `validate` | `(value, row) => boolean` | Return `false` to reject an edit |
324
+ | `comparator` | `(a, b, rowA, rowB) => number` | Custom sort order |
325
+ | `cellRenderer` | `ComponentType<CellParams<T>>` | Component that renders the cell |
326
+ | `cellEditor` | `ComponentType<CellEditorProps<T>>` | Component used instead of the text box when editing |
327
+ | `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 |
335
+ | `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) |
337
+
338
+ ### 6.4 Sorting
339
+
340
+ - 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.
343
+ - Empty values (`null`, `undefined`, `""`) always sort last, in both directions.
344
+ - Add a `comparator` to sort any other way:
345
+
346
+ ```tsx
347
+ { field: "priority", comparator: (a, b) => order.indexOf(a) - order.indexOf(b) }
348
+ ```
349
+
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`.
353
+
354
+ ### 6.5 Searching and filtering
355
+
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.
357
+
358
+ **Column filters.** Filters are set per column, in the column definition. Only columns with `filterable: true` get one:
359
+
360
+ ```tsx
361
+ const columns: ColumnDef<Person>[] = [
362
+ { field: "name", filterable: true },
363
+ { field: "department", filterable: true, filterPlaceholder: "Which team?" },
364
+ { field: "salary" }, // no filter
365
+ ];
366
+ ```
367
+
368
+ - 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.
370
+ - 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.
372
+ - 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.
374
+
375
+ **Styling each indicator separately.** Every column has its own options:
376
+
377
+ ```tsx
378
+ {
379
+ field: "status",
380
+ filterable: true,
381
+ filterIndicatorClassName: "text-amber-300", // Tailwind on this indicator only
382
+ filterIndicatorStyle: { color: "#f59e0b" }, // or inline styles
383
+ filterPopupStyle: { borderColor: "#f59e0b" }, // style this column's popup
384
+ }
385
+ ```
386
+
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`).
388
+
389
+ **A custom indicator component.** Replace the dot with any component. It receives `{ column, active, open }`:
390
+
391
+ ```tsx
392
+ function FunnelIndicator({ active }: FilterIndicatorProps<Employee>) {
393
+ return (
394
+ <svg width="14" height="14" viewBox="0 0 16 16" aria-hidden="true">
395
+ <path d="M1.5 2h13l-5 6v5l-3 1.5V8z" fill={active ? "currentColor" : "none"} stroke="currentColor" strokeWidth="1.5" />
396
+ </svg>
397
+ );
398
+ }
399
+
400
+ { field: "department", filterable: true, filterIndicator: FunnelIndicator }
401
+ ```
402
+
403
+ The component is drawn inside the indicator `<button>`, so do not put another button in it.
404
+
405
+ **A custom filter control.** `filterRenderer` replaces the text box inside the popup (see [filterRenderer](#filterrenderer-custom-filter-control)).
406
+
407
+ Notes:
408
+
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.
411
+ - 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.
413
+
414
+ ### 6.6 Pagination
415
+
416
+ ```tsx
417
+ <DataGrid pageSize={20} pageSizeOptions={[10, 20, 50, 100]} paginationPosition="both" />
418
+ ```
419
+
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".
421
+
422
+ | `paginationPosition` | Result |
423
+ | --- | --- |
424
+ | `"bottom"` | Bar under the table (default) |
425
+ | `"top"` | Bar above the table, below the search box |
426
+ | `"both"` | Both bars, kept in sync |
427
+
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:
429
+
430
+ ```tsx
431
+ <DataGrid pagination={false} maxHeight={400} />
432
+ ```
433
+
434
+ `pageSize` is only read when the grid first mounts. The user's choice in the menu is then kept inside the grid.
435
+
436
+ ### 6.7 Row click, toolbar and empty state
437
+
438
+ ```tsx
439
+ <DataGrid
440
+ onRowClick={(row, rowIndex) => navigate(`/people/${row.id}`)}
441
+ toolbar={<button onClick={exportCsv}>Export</button>}
442
+ emptyMessage="Nothing matches your search"
443
+ striped
444
+ />
445
+ ```
446
+
447
+ ### 6.8 Editing
448
+
449
+ Turn editing on with `editable` on the grid, then lock or unlock columns:
450
+
451
+ ```tsx
452
+ <DataGrid editable columns={[
453
+ { field: "id", editable: false }, // locked column
454
+ { field: "name" }, // editable (grid default)
455
+ { field: "status", editable: ({ row }) => row.department !== "HR" } // decided per cell
456
+ ]} />
457
+ ```
458
+
459
+ A column can also be the only editable one: leave the grid's `editable` off and set `editable: true` on that column.
460
+
461
+ **Starting an edit**
462
+
463
+ | Action | Result |
464
+ | --- | --- |
465
+ | Double-click a cell | Opens the editor, cursor at the end |
466
+ | Select a cell (click or Tab) and press **Enter** or **F2** | Opens the editor, cursor at the end |
467
+ | Select a cell and type a character | Opens the editor with that character replacing the old value |
468
+
469
+ Typing to start does not apply to columns with a custom `cellEditor`. Those open with double-click, Enter or F2.
470
+
471
+ **While editing**
472
+
473
+ | Key | Result |
474
+ | --- | --- |
475
+ | **Enter** | Save, then edit the cell below (the text is pre-selected so typing replaces it) |
476
+ | **Shift+Enter** | Save, then edit the cell above |
477
+ | **Tab** / **Shift+Tab** | Save, then edit the next / previous editable cell, wrapping across rows |
478
+ | **Esc** | Cancel without saving |
479
+ | Click elsewhere | Save |
480
+
481
+ Details:
482
+
483
+ - When there is no editable cell in that direction (for example Enter on the last row of the page), the value is saved and the editor closes. Focus returns to the cell.
484
+ - Set `enterMovesDown={false}` to make Enter save and stay put.
485
+ - Cells locked by `editable` are skipped when moving.
486
+ - If sorting, filtering or paging changes the position of rows while you move between cells, editing follows the row, not the screen position.
487
+
488
+ **How the typed text becomes a value**
489
+
490
+ 1. If the column has a `valueParser`, its return value is stored.
491
+ 2. Otherwise, in a `type: "number"` column, an empty text stores `null`, a valid number stores that number, and anything else is rejected.
492
+ 3. Otherwise the text is stored as a string.
493
+
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.
495
+
496
+ ```tsx
497
+ { field: "email", validate: (v) => /^\S+@\S+\.\S+$/.test(String(v)) }
498
+ ```
499
+
500
+ A value that equals the old value is treated as "no change" and fires no events.
501
+
502
+ **Events.** When a value is saved the grid calls, in this order:
503
+
504
+ ```ts
505
+ onCellEdit({ rowId, rowIndex, field, oldValue, newValue, row }) // row is the updated row
506
+ onRowsChange(nextRows) // the whole updated array
507
+ ```
508
+
509
+ **Keeping your data in sync.** The simplest pattern is state plus `onRowsChange`:
510
+
511
+ ```tsx
512
+ const [rows, setRows] = useState(initialRows);
513
+ <DataGrid rows={rows} onRowsChange={setRows} editable />
514
+ ```
515
+
516
+ Important: the grid copies `rows` into its own state and re-syncs whenever the `rows` prop *changes identity*. If your parent re-creates the array on every render (for example `rows={data.map(...)}` written inline), the grid resets to it each time and edits are lost. Keep `rows` in state or memoize it with `useMemo`.
517
+
518
+ **Saving to a server:**
519
+
520
+ ```tsx
521
+ <DataGrid
522
+ rows={rows}
523
+ onRowsChange={setRows}
524
+ onCellEdit={async (e) => {
525
+ await fetch(`/api/people/${e.rowId}`, {
526
+ method: "PATCH",
527
+ headers: { "Content-Type": "application/json" },
528
+ body: JSON.stringify({ [e.field]: e.newValue }),
529
+ });
530
+ }}
531
+ />
532
+ ```
533
+
534
+ ### 6.9 Custom components per column
535
+
536
+ 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
+
538
+ > **Define these components at module level**, not inline inside your render function. An inline component gets a new identity on every render, so React unmounts and remounts it each time (inputs lose focus, state resets).
539
+
540
+ #### `cellRenderer`: custom cell content
541
+
542
+ Receives `CellParams<T>`:
543
+
544
+ | Prop | Description |
545
+ | --- | --- |
546
+ | `value` | The raw value from the row |
547
+ | `formattedValue` | The value as text, after `valueFormatter` |
548
+ | `row` | The whole row |
549
+ | `rowIndex` | Index of the row in `rows` |
550
+ | `field` | The column's field |
551
+ | `column` | The column definition |
552
+ | `editable` | `true` if the cell may be changed (the column's `editable`, or the grid's) |
553
+ | `updateValue(value)` | Saves a new value for this cell right away, without opening the editor. It runs `validate` and fires `onCellEdit` / `onRowsChange`. Returns `false` if the value was rejected or the cell is not editable |
554
+
555
+ ```tsx
556
+ function StatusCell({ value }: CellParams<Employee>) {
557
+ return <span className={value === "active" ? "text-green-700" : "text-gray-500"}>{String(value)}</span>;
558
+ }
559
+
560
+ { field: "status", cellRenderer: StatusCell }
561
+ ```
562
+
563
+ Searching, filtering and sorting still use the underlying value and `valueFormatter`, not what your component draws.
564
+
565
+ #### `cellEditor`: custom editor
566
+
567
+ Replaces the text box when the cell is being edited. Receives `CellEditorProps<T>`:
568
+
569
+ | Prop | Description |
570
+ | --- | --- |
571
+ | `value` | The current draft value. Starts as the cell's raw value |
572
+ | `row`, `rowIndex`, `field`, `column` | Same as in `CellParams` |
573
+ | `onChange(value)` | Update the draft without saving |
574
+ | `onCommit(value?, move?)` | Save and close. Pass a value to save it, or nothing to save the latest draft. `move` can be `"down" \| "up" \| "left" \| "right" \| "none"` (default `"none"`) and also starts editing the neighbouring cell |
575
+ | `onCancel()` | Close without saving |
576
+
577
+ Rules your editor should follow:
578
+
579
+ - Focus something when it mounts (`autoFocus` or a ref) so the keyboard works straight away.
580
+ - Esc is handled by the grid. Clicking or tabbing out of the editor saves the current draft.
581
+ - A value passed to `onCommit` is stored as is. `valueParser` and the `type: "number"` conversion are not applied, but `validate` still is.
582
+
583
+ ```tsx
584
+ function StatusEditor({ value, onCommit }: CellEditorProps<Employee>) {
585
+ return (
586
+ <select autoFocus value={String(value)} onChange={(e) => onCommit(e.target.value)}>
587
+ <option>active</option>
588
+ <option>pending</option>
589
+ <option>inactive</option>
590
+ </select>
591
+ );
592
+ }
593
+
594
+ { field: "status", cellEditor: StatusEditor }
595
+ ```
596
+
597
+ An editor with its own keyboard handling, saving and moving down on Enter:
598
+
599
+ ```tsx
600
+ function SliderEditor({ value, onChange, onCommit }: CellEditorProps<Employee>) {
601
+ return (
602
+ <input
603
+ autoFocus
604
+ type="range"
605
+ value={Number(value) || 0}
606
+ onChange={(e) => onChange(Number(e.target.value))}
607
+ onKeyDown={(e) => e.key === "Enter" && onCommit(undefined, "down")}
608
+ />
609
+ );
610
+ }
611
+ ```
612
+
613
+ #### `headerRenderer`: custom header label
614
+
615
+ Receives `{ column, sortDirection }`. The filter indicator (if `filterable`) and the sort arrow are still added next to it. The whole header cell is clickable for sorting, so call `event.stopPropagation()` on any interactive element you put inside.
616
+
617
+ ```tsx
618
+ function SalaryHeader({ column }: HeaderRendererProps<Employee>) {
619
+ return <span>{column.headerName} <small>USD</small></span>;
620
+ }
621
+ ```
622
+
623
+ #### `filterRenderer`: custom filter control
624
+
625
+ Shown inside the filter popup instead of the text box (the column still needs `filterable: true`). Receives `{ column, value, onChange }`, where `value` is the filter text. Whatever string you pass to `onChange` is matched with "contains" against the displayed text.
626
+
627
+ ```tsx
628
+ function DepartmentFilter({ value, onChange }: FilterRendererProps<Employee>) {
629
+ return (
630
+ <select value={value} onChange={(e) => onChange(e.target.value)}>
631
+ <option value="">All</option>
632
+ <option>Engineering</option>
633
+ <option>Design</option>
634
+ </select>
635
+ );
636
+ }
637
+ ```
638
+
639
+ #### Checkbox columns (`type: "boolean"`)
640
+
641
+ Set `type: "boolean"` on a column and each cell shows a [Checkbox](#checkbox). One click saves the new `true` / `false` value.
642
+
643
+ ```tsx
644
+ const columns: ColumnDef<Task>[] = [
645
+ { field: "title", headerName: "Task" },
646
+ { field: "done", type: "boolean", align: "center", checkbox: { variant: "button" } }, // no headerName: the header is empty
647
+ { field: "starred", headerName: "Starred", type: "boolean", checkbox: { variant: "button", indicator: "★" } }, // custom character
648
+ { field: "urgent", headerName: "Urgent", type: "boolean", align: "center", checkbox: { variant: "tick" } },
649
+ { field: "archived", headerName: "Archived", type: "boolean", align: "center" }, // grid default: native
650
+ { field: "approved", headerName: "Approved", type: "boolean", editable: false }, // shown disabled
651
+ ];
652
+
653
+ <DataGrid columns={columns} rows={rows} editable onRowsChange={setRows} checkboxVariant="native" />
654
+ ```
655
+
656
+ How it behaves:
657
+
658
+ - **One click saves.** The change goes through the same path as a typed edit: `validate` runs, then `onCellEdit` and `onRowsChange` fire. Space toggles a focused checkbox, and Tab reaches them.
659
+ - **Who can click.** A checkbox is active only where the cell is editable: grid `editable`, overridden per column by `editable: true | false` or a function. Otherwise it is shown disabled.
660
+ - **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
+ - **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
+ - **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`.
664
+ - **Defaults.** 1px border, no rounded corners. See [Checkbox](#5-checkbox).
665
+ - **A column with its own `cellEditor`** is treated as a normal editable column instead.
666
+
667
+ **Choosing and styling the checkbox**
668
+
669
+ | Where | How |
670
+ | --- | --- |
671
+ | Whole grid | `checkboxVariant="tick"` |
672
+ | One column | `checkbox: { variant: "tick" }` |
673
+ | Whole grid, classes or inline styles | `classNames` / `styles` slots `checkbox`, `checkboxControl`, `checkboxIndicator` |
674
+ | 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` |
676
+
677
+ ```tsx
678
+ <DataGrid
679
+ checkboxVariant="tick"
680
+ classNames={{
681
+ checkboxControl: "rounded-full border-emerald-500 aria-checked:bg-emerald-500",
682
+ checkboxIndicator: "text-white",
683
+ }}
684
+ ...
685
+ />
686
+ ```
687
+
688
+ When both are given, the column's options are merged after the grid-level ones, so the column wins.
689
+
690
+ **Fully custom checkbox in a cell.** Any `cellRenderer` can toggle the value itself through `updateValue`. This example is the "button with a span inside" written by hand:
691
+
692
+ ```tsx
693
+ function PinCell({ value, editable, updateValue }: CellParams<Task>) {
694
+ return (
695
+ <button
696
+ type="button"
697
+ aria-pressed={!!value}
698
+ disabled={!editable}
699
+ onClick={(e) => {
700
+ e.stopPropagation(); // keep onRowClick from also firing
701
+ updateValue(!value);
702
+ }}
703
+ >
704
+ <span style={{ visibility: value ? "visible" : "hidden" }}>✓</span>
705
+ </button>
706
+ );
707
+ }
708
+
709
+ { field: "pinned", cellRenderer: PinCell }
710
+ ```
711
+
712
+ #### Select-all header checkbox
713
+
714
+ Add `headerCheckbox: true` to a boolean column. The header label becomes a checkbox (the header name is its label, or no label if the name is empty) that checks or clears the whole column.
715
+
716
+ ```tsx
717
+ { field: "done", headerName: "Done", type: "boolean", align: "center", headerCheckbox: true, checkbox: { variant: "tick" } }
718
+ { field: "done", type: "boolean", headerCheckbox: true } // just the checkbox, no text
719
+ ```
720
+
721
+ - **Click:** if every target row is checked, it unchecks them all. Otherwise (none or only some are checked) it checks them all.
722
+ - **Which rows:** every row that matches the current search and column filters, on all pages, and whose cell is editable. Rows the filters hide are left alone.
723
+ - **State:** checked when all target rows are checked, a dash (mixed) when only some are, unchecked when none are. It updates as you toggle individual rows.
724
+ - **Events:** `onCellEdit` fires once for each row that changed, and `onRowsChange` fires once with the whole updated array. Rows already at the target value, and rows where `validate` rejects the value, are skipped.
725
+ - **Locked columns:** if no cell in the column is editable, the header checkbox is disabled but still shows the real state.
726
+ - **Look:** it uses the same variant and styling as the column's cell checkboxes (`checkbox` options, `checkboxVariant`, and the `checkbox`, `checkboxControl` and `checkboxIndicator` slots).
727
+ - **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
+ - **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
+
730
+ ### 6.10 Styling
731
+
732
+ #### Whole grid
733
+
734
+ ```tsx
735
+ <DataGrid className="rounded-xl shadow-lg" style={{ maxWidth: 900 }} ... />
736
+ ```
737
+
738
+ #### Parts of the grid
739
+
740
+ `classNames` and `styles` take the same keys. Each key is a part ("slot") of the grid:
741
+
742
+ | Slot | Element |
743
+ | --- | --- |
744
+ | `root` | Outer container |
745
+ | `toolbar` | Bar holding the search box and your `toolbar` content |
746
+ | `search` | Global search input |
747
+ | `scroll` | Scrolling wrapper around the table |
748
+ | `table` | The `<table>` |
749
+ | `thead` | The `<thead>` |
750
+ | `headerRow` | The row of column headers |
751
+ | `headerCell` | Each column header (`<th>`) |
752
+ | `filterIndicator` | Each column's filter indicator button |
753
+ | `filterPopup` | The filter popup (drawn in `document.body`) |
754
+ | `filterInput` | The default filter text box in the popup |
755
+ | `tbody` | The `<tbody>` |
756
+ | `row` | Each body row |
757
+ | `cell` | Each body cell (`<td>`) |
758
+ | `editor` | The default text editor input |
759
+ | `empty` | The "no rows" cell |
760
+ | `footer` | The pagination bar (both top and bottom) |
761
+ | `rowCount` | The "1–10 of 200" text |
762
+ | `pageInfo` | The "Page 1 of 20" text |
763
+ | `pageSize` | The rows-per-page `<select>` |
764
+ | `pagination` | Wrapper around the page-size menu and buttons |
765
+ | `pageButton` | Each first/previous/next/last button |
766
+ | `checkbox` | Root of each checkbox in a `type: "boolean"` column |
767
+ | `checkboxControl` | The checkbox's button or input |
768
+ | `checkboxIndicator` | The span inside the `"button"` / `"tick"` checkboxes |
769
+
770
+ #### Tailwind
771
+
772
+ ```tsx
773
+ <DataGrid
774
+ className="rounded-2xl border-0 shadow-xl ring-1 ring-slate-200"
775
+ classNames={{
776
+ toolbar: "bg-slate-50 px-4 py-3",
777
+ search: "rounded-full border-slate-300 px-4",
778
+ headerCell: "bg-slate-900 text-xs uppercase tracking-wider text-slate-100",
779
+ row: "hover:bg-indigo-50",
780
+ footer: "bg-slate-50 text-slate-500",
781
+ pageButton: "rounded-full hover:bg-indigo-100",
782
+ }}
783
+ rowClassName={(row) => (row.status === "inactive" ? "opacity-50" : "")}
784
+ />
785
+ ```
786
+
787
+ You do not need to add the library to Tailwind's `content` setting, because the class names live in your own code.
788
+
789
+ The grid's built-in CSS is written with `:where()`, which gives it zero specificity. That means any class you add, Tailwind utilities included, overrides it without `!important`.
790
+
791
+ #### Inline styles
792
+
793
+ ```tsx
794
+ <DataGrid
795
+ style={{ border: "2px solid #111", borderRadius: 0 }}
796
+ styles={{
797
+ headerCell: { background: "#111", color: "#ffe66d" },
798
+ footer: { background: "#ffe66d" },
799
+ }}
800
+ rowStyle={(row) => (row.status === "pending" ? { background: "#fff7cc" } : {})}
801
+ />
802
+ ```
803
+
804
+ When both are given, `style` wins over `styles.root`.
805
+
806
+ #### Per column and per cell
807
+
808
+ `className` and `style` style a whole column (its header and every cell). The `header*` and `cell*` options target only one part:
809
+
810
+ ```tsx
811
+ {
812
+ field: "salary",
813
+ className: "font-mono text-slate-700", // header and every cell (Tailwind)
814
+ style: { letterSpacing: 0.3 }, // header and every cell (inline)
815
+ headerClassName: "text-right",
816
+ headerStyle: { minWidth: 120 },
817
+ cellClassName: ({ value }) => (Number(value) > 100000 ? "font-bold text-green-600" : ""),
818
+ cellStyle: ({ value }) => (value === null ? { color: "#9ca3af" } : {}),
819
+ }
820
+ ```
821
+
822
+ #### Striped rows
823
+
824
+ ```tsx
825
+ <DataGrid striped /> // default shading
826
+ <DataGrid stripeColors={["#fef3c7", "#e0f2fe"]} /> // your two colours (implies striped)
827
+ ```
828
+
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`).
830
+
831
+ #### Row animation
832
+
833
+ When rows render (the first load, a new page, a sort, a search or a filter), they slide up and fade in, one after another. It is on by default.
834
+
835
+ ```tsx
836
+ <DataGrid animateRows={false} /> // turn it off
837
+ ```
838
+
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`).
840
+
841
+ #### Width
842
+
843
+ The grid is `width: 100%` of its container by default (it never overflows it: `max-width: 100%; min-width: 0`). To make it narrower, give it a width through `className` (`max-w-3xl`) or `style` (`{ maxWidth: 900 }`).
844
+
845
+ #### Plain CSS class reference
846
+
847
+ If you prefer writing CSS, these class names are stable.
848
+
849
+ | Class | Element |
850
+ | --- | --- |
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
868
+
869
+ A column's `field` can point into nested objects with dot notation:
870
+
871
+ ```tsx
872
+ interface Person {
873
+ id: number;
874
+ name: string;
875
+ contact: { email: string; phone: string };
876
+ address: { city: string };
877
+ }
878
+
879
+ const columns: ColumnDef<Person>[] = [
880
+ { field: "name" },
881
+ { field: "contact.email", headerName: "Email", filterable: true },
882
+ { field: "contact.phone", headerName: "Phone" },
883
+ { field: "address.city", headerName: "City" },
884
+ ];
885
+ ```
886
+
887
+ - **TypeScript checks the path.** `field` is typed `FieldPath<Person>`: `"contact.phone"` is accepted, `"contact.fax"` is a type error. It follows up to 5 levels deep, and arrays and dates are treated as plain values.
888
+ - **Everything uses the path:** display, global search, filters, sorting, editing, `valueFormatter`, `cellRenderer` (`value`, `field`), and boolean columns and select-all headers (`"prefs.newsletter"`).
889
+ - **Editing never mutates your data.** Saving `contact.phone` copies `row` and `row.contact` and replaces the value, so the original objects are left alone. A missing level on the way is created. `onCellEdit` reports `field: "contact.phone"` and the updated row.
890
+ - **A missing level reads as empty.** If `row.contact` is `undefined`, the cell is blank and sorts last. No error is thrown.
891
+ - A key that really contains a dot (`{ "a.b": 1 }`) is read as that literal key first.
892
+ - `__proto__`, `prototype` and `constructor` are never read or written through a path.
893
+ - `getRowId`, `onRowClick` and other callbacks work as before.
894
+
895
+ ### 6.12 Keyboard reference
896
+
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 |
906
+
907
+ Arrow-key navigation between cells is not implemented. Use Tab, or the mouse, to select a cell.
908
+
909
+ ### 6.13 Accessibility
910
+
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.
916
+
917
+ ### 6.14 TypeScript
918
+
919
+ All types are exported:
920
+
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";
944
+ ```
945
+
946
+ Typing tips:
947
+
948
+ ```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
951
+ ```
952
+
953
+ ### 6.15 Recipes
954
+
955
+ **Read-only grid**
956
+
957
+ ```tsx
958
+ <DataGrid columns={columns} rows={rows} /> // editable defaults to false
959
+ ```
960
+
961
+ **Currency column that accepts "$1,200" when typed**
962
+
963
+ ```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**
977
+
978
+ ```tsx
979
+ <DataGrid columns={[{ field: "id" }, { field: "notes", editable: true }]} />
980
+ ```
981
+
982
+ **Lock rows after approval**
983
+
984
+ ```tsx
985
+ { field: "amount", editable: ({ row }) => !row.approved }
986
+ ```
987
+
988
+ **Scrolling grid with sticky header**
989
+
990
+ ```tsx
991
+ <DataGrid pagination={false} maxHeight="60vh" />
992
+ ```
993
+
994
+ **Highlight a row based on its data**
995
+
996
+ ```tsx
997
+ <DataGrid rowClassName={(row) => (row.overdue ? "bg-red-50" : "")} />
998
+ ```
999
+
1000
+ ### 6.16 Known limitations
1001
+
1002
+ Not included (yet):
1003
+
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)
1011
+
1012
+ Behaviours to be aware of:
1013
+
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`.
1022
+
1023
+ ---
1024
+
1025
+ ## 7. Troubleshooting
1026
+
1027
+ | Problem | Cause and fix |
1028
+ | --- | --- |
1029
+ | 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 |
1032
+ | 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
+ | 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 |
1035
+ | 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
+ | 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 |
1038
+ | Typing in a custom editor column doesn't start editing | By design: custom editors open with double-click, Enter or F2 |
1039
+ | 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 |
1042
+ | 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
+ | 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` |