Repository navigation
Changelog
📝 Generated from
CHANGELOG.md. Edit it there; changes made in the wiki are overwritten.
Package: @opencorestack/opengridx
License: MIT © 2026 Open Core Stack
-
RTL: header filter operator symbols are no longer mirrored. The browser's bidi rules drew
∋(contains) as∈(is any of) in right-to-left grids, so the two operators swapped their look. The operator button is now laid out left to right in every direction.
-
Select all at 1,000,000 rows ~60–90 ms (3.10.0: ~130 ms), deselect all under 50 ms (~95 ms). The check that drops selected ids of removed rows now skips a selection it already checked against the row store's current maps, and selections the grid built from it by adding rendered rows (checkbox, row click, select-all). Rows are removed only by replacing the rows, which gives the store new maps, so any replacement still triggers the full check; ids set through
apiRef.selectRowsare always checked. Deselecting all after selecting all returns the empty selection without deleting 1M ids one by one. -
Grouping 1,000,000 rows takes ~190 ms instead of ~540 ms. Leaf rows are recorded as one id → row-index entry; their tree nodes and
GridRowMetaobjects are made the first time something reads them (only rendered rows do) and kept, so each row still has exactly one metadata object. Rows are partitioned by index, so each leaf's id comes from the row store's id list instead of a lookup per row. -
The quick filter's first search at 1,000,000 rows takes ~1.6–2 s instead of ~2.7 s, later keystrokes ~210 ms instead of 250–530 ms. Locale dates are formatted once per calendar day instead of once per cell (Intl formatting was ~0.5 s of it), and the shown texts of date, boolean and select columns are lower-cased once per distinct text; a select option whose label is its value is stored once. What remains is reading and formatting every cell once (your
valueFormatterincluded) for the per-row text cache that makes later keystrokes fast. For instant search over 1M rows use adataSource.
-
A unit test runs row grouping on 5,000 rows and checks that the whole-dataset pass creates no leaf tree node and that a row's metadata object is made once and reused, so the lazy leaf maps cannot silently turn eager again.
-
Next.js fixture in the package smoke suite (
e2e/fixtures/nextjs,e2e/nextjs.spec.ts): a Next.js 16 App Router app built withnext build(Turbopack) against the packed tarball, without the optional export peers and withoutnext.configaliases. In Chromium, Firefox and WebKit the server-rendered HTML must contain the grid, the page must hydrate without console errors, sorting must work andexportToExcelAdvancedmust reject with its install hint. The claims ofdocs/features/nextjs.mdwere checked once by hand before; CI now re-checks them on every run.
-
The AI assistant's change chips are translatable: 11
localeTextkeys (aiAssistantChipSearch,ChipNoFilters,ChipSort(field, direction),ChipNoSorting,ChipGroup,ChipNoGrouping,ChipSummary(fn, field),ChipNoSummaries,ChipPivot,ChipNoPivot,ChipVisibility(field, visible)), English by default; filter operators in the chips usefilterPanelOperators.buildAiChipstakes the texts as an optional third argument.
-
Select all at 1,000,000 rows takes ~130 ms instead of ~260 ms (deselect ~95 instead of ~120), with no API change. The selection set built for the new model is reused when that array comes back as
rowSelectionModel(instead ofnew Set(model)again), and a set built by selecting all rows is known to contain them, so the header checkbox's "all selected" count is skipped. The shared selection set is typedReadonlySetinternally, so nothing can change it in place.
-
Guards that fail CI instead of relying on review:
check:versions(llms.txt, CLAUDE.md and CHANGELOG must name thepackage.jsonversion),check:compiler(fails on any React Compiler result outside a short allowlist, so a refactor that stops it from optimizing a hook is caught), ESLintno-explicit-anyturned on (it was off; the library had none, the demo's 9 are now typed) and a rule against newas unknown ascasts in the library outside the documented boundary files. -
Refactors (no behaviour change):
GridToolbarsplit into its panels with a shareduseToolbarPanel(861 → 492 lines); the exporters one file per format; the xlsx cell helpers inexcelCells; the cell range drag inuseGridCellRangeDrag; keydown handling in a plain, unit-testedhandleGridKeyDownfunction. Type castsas unknown as43 → 15: the row pipeline, data source and grouping are called with the row type, the row store is typed with it, and column widenings go through one documented helper. DOM, styles and behaviour unchanged (theme baseline and the full browser suite in three engines). -
Demo: the source viewer on every example page is syntax-highlighted (
CodeBlock, prism-react-renderer), and the integration tips under it are corrected (inline columns and handlers are fine since 3.0 / 3.9; replace rows instead of mutating them).
-
The npm package no longer ships the theme test baselines.
lib/theme/__baseline__/(three JSON files, 1.6 MB, added in 3.7.0) was published becausefilesexcluded tests, screenshots and snapshots but not that folder. Unpacked size 9.4 → 7.8 MB. - The demo's home page badge showed v3.7.0; it and the header now read the version from
package.json.
-
npm run check:package, run in CI and before publishing: fails whennpm packwould ship a test artifact or whenDataGridalone, minified and gzipped, grows past its budget (92 KB; 85.5 KB now).
- Roadmap brought up to date (3.5.1 – 3.9.1 shipped; new features on hold until the backlog of fixes, refactors and performance work is closed; the 4.0 feature-modules proposal).
docs/performance.md: edit, paste, fill, group toggle and select-all timings at 1M rows and how to keep rows memoised. README: spreadsheet editing, Ask AI panel,localeText, RTL and Theme Builder in the feature list; bundle sizes re-measured.rtl.mdandcomponents/pagination.mdno longer say their labels cannot be translated. Architecture notes on the column-window memo and the locale context. The wiki home links the newer feature pages.
-
Nine theme keys (additive; each defaults to exactly what the grid draws today, checked by the default-theme baseline in Chromium, Firefox and WebKit):
grid.checkboxCheckedBackground(--ogx-checkbox-checked-bg, the fill and border of a checked or indeterminate checkbox, default the primary colour),grid.checkboxCheckmark(--ogx-checkbox-checkmark, default white),grid.expandIconColor/expandIconHoverColor(--ogx-expand-icon-color/-hover-color, the group and tree chevron, default header text / row text), and for menusoverlays.itemIndicatorColor(--ogx-overlay-item-indicator, the selection dot, default the selected item text),itemSelectedIcon(--ogx-overlay-item-selected-icon, default primary),iconColor(--ogx-overlay-icon, default gray-400),sectionLabelText(--ogx-overlay-section-label-text, default gray-400) anddividerColor(--ogx-overlay-divider, default the overlay border). -
Theme Builder controls for those keys and for existing keys it did not offer: a Checkboxes group (background, border, checked fill, check mark), a Group and tree rows group (chevron, chevron hover), Selected row hover, a Toolbar and search box group (toolbar text, search box background, text, border, focus border), and in Column menu and panels the item hover text, selected item text, selection dot, selected item icon, icons, section labels, dividers and danger item colours.
-
localeTextcovers every built-in UI string (196 optional keys, additive; without them the grid shows the same English as before): the column menu, toolbar and quick search, filter panel (including operator names), columns panel, pivot panel, AI assistant panel and its status line, pagination buttons, row and header checkboxes and handles, cell selection statistics, list view, loading and error overlays, the error cell and screen-reader announcements. The strings are resolved once per grid and shared through a context, so rows read them without a new prop. Components used outside a grid (GridToolbar,ColumnVisibilityPanel,FilterPanel) show English; their explicit props still win. Not covered: the AI assistant's change-chip texts, the grouping column's default header (groupingColDef.headerNamesets it), and row data or file contents such as the pivot "Grand Total" row and export subtotals. An inlinelocaleTextobject of strings keeps the rows memoised; function entries are compared by identity. See Locale text for every UI string indocs/API_REFERENCE.md; the Locale Text demo shows a French grid.
-
Sidebar reorganised by what developers look for first: Getting started (with Basic Usage and the API reference), Sorting & filtering, Editing, Pagination & server data, Export & clipboard, Grouping & aggregation, Selection & spreadsheet, Layout, Theming & customization, AI, Advanced, Tutorials, then Resources (licensing, support, migration guides). Page URLs are unchanged. Text pages are marked
doc(full width) instead of relying on their category. - The header shows the package version from
package.json(it was fixed at v3.7.0).
-
Selecting a row, starting or committing an edit, and scrolling no longer re-render every visible row. Three props broke the rows' memoisation: the row click and checkbox handlers changed identity on every selection change (they now keep one identity and read the latest handlers when called),
editingCellwas passed to every row (now only to the row being edited), and the visible column list was rebuilt on every row change or vertical scroll (it now has its own memo over column widths and the horizontal scroll). Measured on the editing demo: a click re-rendered all 14 visible rows, now 1; starting an edit re-rendered all rows twice, now 2 renders in total. Guarded byDataGrid.rowMemo.test.tsx.
- Four tests that failed now and then under load are steadier: the row and column drag-reorder browser test releases the mouse before and after each test and waits for layout; the header-filter scroll test clicks at the cell's coordinates (Playwright's click scrolled the cell into view, which re-rendered the virtualized rows and detached it); the PDF heartbeat test takes the better of two runs; the tree-data linear-time test interleaves its two sizes over five rounds.
-
Buttons inside the grid show their own background and border again. A reset rule from the first release (
.ogx button:not(.ogx-menu-item)) out-ranked the buttons' own classes and removed their background and border: the AI assistant's Ask button had no fill (dark text on the dark panel in dark mode), its suggestions and the Undo / microphone buttons had no outline, and the open or active states of the toolbar's Columns / Filters buttons and the header filter's operator button had no background. The reset's background and border now have zero specificity (:where()); its font and cursor rules are unchanged. The default-theme baseline (lib/theme/__baseline__/) was regenerated: the only differences are those three open / active backgrounds. -
bigintvalues sort exactly. Values beyond ±2^53 were compared as their nearest double, so neighbours could tie or swap. When a column holds such a value, its numbers are ranked with an exact comparison (other columns are unchanged). -
Number filters no longer read
0x10,0b1,0o7,Infinityor1e999typed as text as numbers (toNumberinlib/utils/values.ts, which number filters, the number column's quick-filter and sorting coercion share). - The AI assistant's suggestion pills and change chips were drawn as ellipses: they read
--ogx-border-radius-round, which is50%. They use a fixed pill radius now.
- Removed code nothing called: seven pinning helpers (
isColumnPinnable,pinColumn,unpinColumn,getPinnedColumnsWidth,pinRow,unpinRow,getPinnedRowsCount),isRowMatchingFilter,getDefaultOperator,compareRowsBySortModel,isSingleCellRangeand the emptylib/theme/defaultStyles.ts(none were exported from the package). Removed the CSS rules of nine classes the grid never sets:ogx--dark,ogx--density-compact,ogx--density-comfortable,ogx--dragging,ogx--list-auto,ogx--loading,ogx__cell--editable,ogx__header-cell--focus-visibleandogx__menu-icon-btn--active. CSS variables are kept (they are public tokens). - Infinite-scroll placeholder rows are drawn through a
skeletonprop on the internalRowinstead of a_isSkeletonfield, which is gone fromGridRowModel(it was marked internal; the type's index signature still accepts any field). - The filter panel and the header filter row share their value helpers (
lib/utils/filtering/filterInput.ts) instead of two copies.
-
llms.txtupdated to 3.8.1: corrected the row-limit note (Firefox's lower limit, the 3.5.1 height cap) and the RTL note (pinned columns work since 3.6), and added notes on replacing rows instead of mutating them, large-grid timings, styling buttons inside the grid and the quick filter's displayed text. -
docs/API_REFERENCE.mdcovers every export of the package: the export option types (CsvExportOptions,JsonExportOptions,ExcelExportOptions,ExcelAdvancedExportOptions,ExcelSheetDefinition,ExcelColumnStyle), the theme part types andDataGridThemeProviderProps, andGridAlignment,GridSortDirection,GridSortModel,GridPinnedPosition.
- Cell edits, paste, undo, redo and fill no longer re-process every row. A committed edit updates the row store's id maps in place and patches only the edited positions of the row array, instead of copying two maps and rebuilding the array. The row-reorder index comes from the store. The id lists that cell selection, paste / fill / undo targets and row selection derive from the displayed rows are reused when an edit leaves every row at its position with its id (a reference comparison per row, a lookup only for replaced rows). Measured at 1,000,000 rows in Chromium (Apple M1): one cell edit 561 → ~35 ms, paste of 1,000 × 10 cells ~790 → ~45 ms, undo / redo ~790 → ~45 ms, fill of 1,000 cells 809 → ~50 ms.
-
Expanding and collapsing a group no longer rebuilds row metadata for every row. Leaf rows'
GridRowMetaobjects are built once per grouping tree; a toggle rebuilds only the group rows' entries and reads both through one read-only map. At 1,000,000 rows: expand 279 → ~135 ms, collapse ~120 → ~1 ms. - Row selection checks for removed ids before copying the selection, so selecting all rows no longer copies the selection once more.
-
docs/features/filtering.md(Quick Filter): the searched text is cached per row object, so rows must be replaced, not mutated, and avalueFormatterthat reads outside state needs new column definitions when that state changes.
-
GridInternalState.rowsalso holdsrows(the stored rows in order) andindexById.GridApi.getAllRows()returns a copy of it. Newlib/utils/rowIndex.ts(getRowIndex) andlib/utils/grouping/layeredMap.ts(LayeredMap); internalrowMetaMapparameters are typedReadonlyMap.
-
Exports no longer crash when a column's
valueGetterthrows. CSV, Excel (HTML andexportToExcelAdvanced), JSON, print and PDF read cell values throughgetCellValue, as the grid does: a throwing getter exports an empty value with a one-time development warning. -
Formula escaping covers
valueFormattertext on number, date and boolean columns (security). A formatter returning=HYPERLINK(...)for a number column was written to CSV and HTML-Excel unescaped, because only the raw value's type was checked. Formatter text is now left unescaped only when it reads as a plain formatted number (digits, signs, separators, brackets,%, currency symbols), so-$3stays as is. -
The quick filter matches the text the grid shows for
singleSelect(option label),boolean(Yes / No) anddate(locale date) columns without avalueFormatter, in addition to the raw value. -
Free text is no longer read as a date.
Date.parseaccepts text such ashello 5(a 2001 date in V8); pasting it into a date column stored that date. Date text may now only contain month and weekday names,T,Z,AM/PM,GMT/UTCbesides digits and punctuation. The same rule applies to date filters and sorting of date strings. -
Pasting a date into a cell that holds a UTC date-time (
…Zor±hh:mm) kept the time part but changed the zone meaning, so west of UTC the cell showed the day before. The pasted day now keeps the cell's local time of day and is written as UTC. -
List view follows the theme. Its row text read the undeclared
--ogx-grid-cell-text, and hover / selection used fixed colours; they now use--ogx-grid-row-text,--ogx-grid-row-hover-background,--ogx-grid-row-selected-backgroundand--ogx-grid-row-selected-hover-background(--ogx-selection-bg/--ogx-selection-hover-bgstill win when set). -
The stylesheet no longer changes the host page. The
box-sizing: border-boxreset applied to every element on the page; it now applies only to elements with anogxclass and their descendants (including portalled menus and panels), at zero specificity. A page that relied on it for its own elements (for example a padded container with a fixed width around the grid) may need its ownbox-sizing: border-boxrule.
- The grid no longer draws a shadow on hover (
.ogx:hover), which repainted the whole grid on every pointer enter and leave. Add.ogx:hover { box-shadow: var(--ogx-shadow-md); }to keep it.
-
useLayoutcomputes row heights and column widths in separate memos: resizing a column no longer walks every row, and sorting or filtering no longer recomputes column widths.
-
16 theme keys for fonts, dividers and corners (additive; every one defaults to exactly what the grid drew before).
grid.headerFontFamily/headerFontWeight/cellFontFamily/cellFontWeight(--ogx-grid-header-font-family,--ogx-grid-header-font-weightdefault 600,--ogx-grid-cell-font-family,--ogx-grid-cell-font-weight),grid.headerPaddingX(--ogx-grid-header-padding-x, defaults to the cell padding),grid.borderWidth(--ogx-grid-border-width, 1px) andgrid.borderRadius(--ogx-grid-border-radius, defaults to--ogx-border-radius-lg),grid.columnDividerWidth/headerColumnDividerWidth/rowDividerWidth(--ogx-grid-column-divider-width,--ogx-grid-header-column-divider-width,--ogx-grid-row-divider-width, 1px;0hides them) andgrid.columnDividerColor/rowDividerColor(--ogx-grid-column-divider-color,--ogx-grid-row-divider-color, default the grid border colour), andoverlays.fontFamily/fontWeight/fontSize(--ogx-overlay-font-*) for the column menu, header filter menu, toolbar panels and AI panel. The hard-coded 1px divider borders of cells, rows, header rows, column-group rows, the header filter row, aggregation footer cells and the right-pinned edge now read these variables (logicalborder-inline-*properties kept). A new browser test (lib/theme/defaultThemeBaseline.browser.test.tsx) compares computed styles and element boxes of eleven grids (plain, pinned with column groups and selection, dark and compact presets, header filters and their menu, aggregation footer, toolbar panels, column menu) with a baseline captured before the change, in Chromium, Firefox and WebKit: no differences. Guide: Fonts, dividers and corners and the hide the dividers recipe indocs/customization/theming.md. -
Theme Builder demo page (
/theme-builder, under Customization, linked from the Theming page). Grouped, collapsible controls with a reset each: colour picker plus text field, font family (presets or custom), weight 300–800 and size, sliders for heights, padding, widths and radius, and show/hide switches for body column dividers, header dividers and row dividers (they set the width to 0). A live preview grid (toolbar, pinned column, selected row, row grouping, density switch, a button that opens the column menu) inDataGridThemeProvider. A Light / Dark switch picks which theme is edited and previewed; "Start from" loads a built-in preset. Copy theme (TypeScript, only values that differ from the defaults, light, dark or both; clipboard API with a fallback), Download.ts, Share via URL (#theme=…, restored on load). WCAG AA contrast hints for seven text/background pairs (warnings only) and a collapsed Advanced JSON editor for every theme key. Controls move into a drawer when the page is narrower than about 1,060 px. The demo smoke test also runs it in Firefox and WebKit.
-
DataGrid.tsxsplit up (1,128 → 620 lines), with no change to the public API, the rendered DOM, attributes, class names, event order or hook order. The JSX moved intoGridRoot(root element, toolbar, panels, list view, footer, live region, overlays) andGridViewport(therole="grid"viewport and the shared row renderer props). Seven composite hooks each wrap a contiguous run of the documented hook order and call it unchanged:useGridEditPipeline(12a–13),useGridSelectionAndStateApi(23–24),useGridViewApis(30–31a),useGridScrollViewport(32–33),useGridRangeEditing(36a–36d),useGridRenderWindow(38–41) anduseGridToolbarStack(44–46).docs/architecture/datagrid-orchestration.mdlists each hook with its composite. React DevTools shows the two new components (GridRoot,GridViewport) underDataGrid; each renders once per grid render. -
Demo-site smoke test (
npm run test:demo,e2e/demo.spec.ts,e2e/playwright.demo.config.ts). Builds the demo and serves it withvite previewon port 4322 under/OpenGridX/, then loads every route ofdemo/App.tsx(read from the source; 55 routes, checked against the built sidebar) in Chromium, and seven heavy pages (cell selection, AI assistant, full feature test, RTL, theming, pivot, infinite scroll) also in Firefox and WebKit. Each page must log no console error or warning and no page error, replace the loading skeleton, show text, and every grid must show a row or an intentional empty / loading / error state. Runs inci.ymlafter the package smoke suite and indeploy.ymlin place of the bare demo build, so a broken demo page is never deployed. No demo page was broken. -
Height-limit guidance. The development warning for content taller than the browser lays out now names
dataSource(server-side pages or infinite scroll) withdocs/features/data-source.md, thenpaginationand compact density, and links the new The browser height limit section ofdocs/features/virtualization.md. That section opens with a decision guide: up to about 300,000 rows at 52 px fit in every browser, up to about 600,000 in Chromium and WebKit only, beyond that use adataSourceor pagination;density="compact"stretches each limit about 1.6×.docs/performance.mdlinks it. New unit testuseGridDevWarnings.test.tscovers the warning text. -
source-map-jspinned to 1.2.2 throughoverridesin the root and in the e2e and bench fixtures (GHSA-68fv-2mgg-jv7q, high, dev toolchain only; the root lockfile had 1.2.1). Thesprintf-jsadvisory (GHSA-hp3w-g68c-fv3c, no patched release) stays open: it comes from@microsoft/api-extractor→@rushstack/ts-command-line→argparse@1, whichvite-plugin-dtsv5 still needs to bundledist/index.d.tsanddist/ai.d.ts(api-extractor 7.59.4 still depends onargparse ~1.0.9), so upgrading to v5 would not remove it. It only runs at build time on this repository's own sources.
-
Translatable header filter row. New
GridLocaleTextkeys for every string the header filter row shows or announces:headerFilterCustom/headerFilterCustomTooltip("Custom filter"),headerFilterCellLabel,headerFilterValueLabel,headerFilterOperatorButtonLabel,headerFilterOperatorMenuLabel,headerFilterClearLabel(aria-labels, functions of the column header),headerFilterClearTooltip,headerFilterAny/headerFilterTrue/headerFilterFalse(the selects) andheaderFilterOperators(operator names). Each defaults to the English text it replaces. The Locale Text demo shows them in French.
- The column resize handle now releases its pointer capture when a resize ends. A capture left behind (for example by a press that never got a real
pointerup) could keep the pointer's later events from reaching the rest of the page. -
Apps without the optional export peers failed to build in webpack and Next.js (
Module not found: Can't resolve 'exceljs'fromnext buildon Next.js 15 and 16, and from plain webpack 5). Theexceljsimport inexportToExcelAdvancednow sits inside atryblock, as thejspdfimports already did, which webpack and Turbopack treat as an optional dependency: apps withoutexceljs,jspdfandjspdf-autotablebuild (webpack shows a warning) andexportToExcelAdvancedrejects withexportToExcelAdvanced requires 'exceljs'. Run: npm install exceljs. Apps that install the peers are unchanged. Thenext.config.tsaliases in the Next.js guide are no longer needed. The package smoke suite has a third fixture,e2e/fixtures/nopeers(webpack 5, no peers), that must build, render the grid and get the install hints in Chromium, Firefox and WebKit. -
validateGridAiStatereported a top-levelmessageas "Unknown part" when given a JSON string, and also for an object passed directly. A stringmessage(the model's reply text) is now skipped in both forms; a non-stringmessageis reported asExpected text, got ….createGridAiPromptHandlernow also returns amessagekey found inside a JSON string reply or a fenced JSON block asresult.message(joined with the text around the block when both exist), as it did for object replies. The Next.js guide no longer stripsmessagebefore validating. -
Importing the grid from a React Server Component crashed the build (
TypeError: createContext is not a functionin Next.js App Router "collecting page data").dist/opengridx.es.jsnow starts with'use client', so a Server Component gets a client reference:<DataGrid>with serialisable props can be rendered from a server file, and the grid hydrates in the browser. Wrapping it in your own'use client'file (needed for column functions) works as before.scripts/check-bundle.mjschecks the directive. The UMD build is minified and does not carry it; Next.js and other RSC bundlers load the ES build. -
Right-to-left grids (
dir="rtl"on an ancestor or<html>). Unpinned RTL grids now work: horizontal scrolling no longer leaves the body blank (column virtualization read the negative RTLscrollLeftas 0; offsets are now measured from the start edge, also for keyboard scroll-into-view andscrollToIndexes); ArrowLeft / ArrowRight (and Shift+Arrow ranges, Alt+Arrow expand / collapse and column resize) follow the screen direction; cell text usestext-align: start/end(align: 'left'/'right'mean start / end); the resize handle sits on the column's left edge and dragging left widens it; the fill handle sits on the range's bottom-left corner; the range outline is drawn on its outer edges; dragging a range past the left edge auto-scrolls; toolbar panels open rightwards from their button. Left-to-right grids are unchanged (logical CSS properties, or code that runs only when the computed direction isrtl; a browser test compared 415 element positions in Chromium, Firefox and WebKit before and after). -
Pinned columns in right-to-left grids.
pinnedColumns.left/.rightnow mean the inline start / end: in RTL left-pinned columns (and the checkbox, expand and reorder columns pinned withpinCheckboxColumn/pinExpandColumn) stick to the right edge and right-pinned ones to the left edge, in the header, column group rows, header filter row, body, skeleton rows and aggregation footer. They used physicalleft/rightsticky offsets and scrolled away. Pinned shadows and section borders face the scrolling columns, range drag auto-scroll stops at the pinned sections on either side, and the column menu's Pin to Left / Pin to Right pin by screen side (in RTL Pin to Left adds topinnedColumns.right). Left-to-right is pixel-identical: a browser test compared 2,211 element boxes, shadows, borders and pseudo-element offsets (pinned columns, system columns, column groups, header filters, footer; before and after a scroll and with a range) in Chromium, Firefox and WebKit before and after. - Mirrored icons in RTL. Pagination first / previous / next / last arrows and the tree / group expand chevron mirror in RTL; tree indentation is on the start side. Sort arrows and other non-directional icons are unchanged.
-
Switching
diron a mounted, scrolled grid left the body blank in Firefox (it resetsscrollLeftwithout a scroll event); the grid now re-reads the scroll position when adirattribute changes. -
RTL browser tests failed on Linux CI by 10 px. Test-only: Playwright's headless Chromium hides scrollbars, but with classic scrollbars
scrollbar-gutter: stablestill reserves the gutter and in RTL Chromium then cannot scroll to the start (scrollLeftstops at minus the gutter; a plain scroller does the same). The tests now measure against the browser's scroll origin and scrollport.
-
Right-to-left guide —
docs/features/rtl.md: what works in RTL, what was fixed, and the known limitations with workarounds (do not pin columns in RTL grids). New demo page Right-to-Left (/rtl) with Arabic and Hebrew data and an RTL toggle. -
Next.js (App Router) guide —
docs/features/nextjs.md: the'use client'grid file, the stylesheet inapp/layout.tsx, server-fetched rows as props (columns with functions stay in the client file),dataSourcewith a Route Handler,@opencorestack/opengridx/aion the server and in the browser (the validation brand does not survive JSON, so the client validates), the Pages Router, and the common errors. Verified withnext build+next starton Next.js 16.4 (Turbopack) and 15.5 (webpack), React 19.3, in Chromium, Firefox and WebKit. It also covers the optional export peers (not needed to build; see Fixed).
-
Faster sort, quick filter and grouping on large data; results unchanged. Row order, filtered rows and group contents are the same as before (randomized tests compare them with the previous implementation). Medians at 1M rows (
npm run bench, Apple M1, before → after,docs/performance.md→ Benchmarks):- Sort by a number column: Chromium 1,716 → 467 ms, Firefox 1,935 → 468 ms, WebKit 1,126 → 340 ms.
- Sort by a string column: Chromium 1,510 → 467 ms, Firefox 2,338 → 560 ms, WebKit 2,939 → 562 ms. Strings are ranked by sorting only their distinct values with the collator instead of comparing every row pair with it.
- Quick filter, first keystroke: Chromium 2,127 → 1,813 ms, Firefox 3,283 → 2,789 ms, WebKit 3,646 → 3,166 ms.
- Group by a column +
sum: Chromium 924 → 693 ms, Firefox 1,879 → 1,776 ms, WebKit 818 → 675 ms. - Also faster: number filters (144 → 97 ms in Chromium) and mount (804 → 621 ms), from reading plain cells without allocating and computing row heights in one pass.
-
docs/superpowers/specs/2026-10-12-nonblocking-pipeline-proposal.md: a proposal for time-sliced sorting, filtering and grouping (not implemented). -
lib/types/index.tsis split into one file per domain (rows,columns,sorting,filtering,editing,export,dataSource,selection,grouping,state,locale,props,slots,api,ai);index.tsre-exports them. A move only: the publishedindex.d.tsandai.d.tsdeclare the same types with the same doc comments.
-
Large grids did not scroll at all in Firefox. Firefox lays out elements up to about 17.9M px and collapses a taller one instead of clamping it, so a grid with more content (about 344,000 rows at the default 52 px row height, 559,000 at 32 px) showed only its first screen of rows and had nothing to scroll. The grid now measures the browser's height limit once per page (an invisible probe,
lib/utils/maxElementHeight.ts) and caps its scroll content to it: Firefox scrolls normally and reaches every row above the limit, as Chromium and WebKit already did up to theirs (about 33.5M px). Rows below the limit are still not reachable in a single scrolling grid; use pagination or server-side loading. Nothing changes for grids under the limit. - The dev warning about very tall content now appears only when rows are actually cut off in the current browser, and says how many rows can be scrolled to ("Only the first 344,000 of 500,000 rows can be scrolled to in this browser").
-
Large-dataset benchmark —
npm run bench(bench/run.mjs) packs the library, installs it intobench/app(a Vite + React 19 production app, like the smoke fixtures; port 4320) and measures it with Playwright in Chromium, Firefox and WebKit at 10k, 100k, 500k and 1M rows (1M also at 32 px): mount, sort, filter, quick filter, group +sum, a scripted 5 s scroll (frame-time percentiles and missed frames), DOM size, the furthest reachable row, and long tasks and JS heap in Chromium. Times run from the call to the first painted frame showing the result; medians of 5 runs after a warm-up.bench/report.mjswrites the Markdown tables; results and method are indocs/performance.md→ Benchmarks. Flags--rows,--engine,--repeat,--quick. The weekly workflow runs a Chromium--quickpass and uploads the JSON; it never fails on numbers. Packing and tarball install are shared with the smoke suite (scripts/lib/package-fixture.mjs). -
Docs: the browser height limit, as measured —
docs/performance.md,docs/features/virtualization.mdand the README now give the benchmark's numbers instead of unmeasured claims ("60fps for 100k+ rows"), and correct the Firefox limit: past about 17.9M px of content (about 344,000 rows at 52 px) Firefox does not stop at the cap like Chromium and WebKit; the content collapses and the grid does not scroll at all.
-
AI toolkit, part 2: the Ask AI panel — new
aiAssistantprop ({ onPrompt, placeholder?, suggestions?, voice?, parts? }).GridToolbarshows an Ask AI button (localeText.aiAssistantButton) that opens a prompt panel under the toolbar;apiRef.openAiAssistant()/closeAiAssistant()open and close it without a toolbar, andslots.aiAssistantPanelreplaces it (GridAiAssistantPanelProps). The panel has a prompt input (Enter sends), suggestion chips, a microphone button only where the browser hasSpeechRecognition(the microphone is requested on click, never on load;voice: falsehides it), Stop (abortscontext.signal), the reply text, the session's earlier prompts, and the list of ignored parts. A reply is applied at once as one undoable step: every change is a removable chip (removing one re-applies the reply without it) and Undo restores the models from before the reply.role="dialog"labelled "Ask AI", focus to the input on open and back on close, Escape closes, status in the grid's live region. NewonAiAssistantApply(result)andonAiAssistantError(error). Grids withoutaiAssistantare unchanged, and the core bundle still holds no AI code. -
Only validated replies are applied.
validateGridAiStatenow brands its result and the result'sstatewith the non-enumerableSymbol.for('opengridx.ai.validated')(shape, equality and JSON unchanged); the panel applies a state only when it carries it, and otherwise shows "The reply was not validated" with a development warning. -
createGridAiPromptHandler({ columns, callModel, parts?, schemaOptions? })in@opencorestack/opengridx/ai— buildsonPromptfor the app's own model: the schema once, thencallModel({ prompt, schema, currentState, history }, { signal }); the reply may be a state object, a JSON string or text with a fenced JSON block (the text or amessagekey becomes the reply message). AcallModelfailure becomes an error result; an abort rejects with anAbortError. -
createGridAgentTools(apiRef, columns, options?)in/ai— plain tool objects (name,description,inputSchema,execute) for the Vercel AI SDK, CopilotKit, OpenAI or Anthropic tool calling and WebMCP:get_grid_state,set_filter,set_sort,set_grouping,set_aggregation,set_column_visibility,clear_filters, andget_row_summaryonly withallowRowAccess: true(the one tool that sends row data). Inputs are validated and applied all or nothing;executenever throws. -
GridApi:setSortModel,setRowGroupingModel,setAggregationModel,setColumnVisibilityModel,setPivotModel(each fires its change callback) andgetGridAiState(), plusopenAiAssistant()/closeAiAssistant(). -
onRowGroupingModelChange—rowGroupingModelis now a controlled/uncontrolled pair: without the prop the grid keeps its own grouping model, whichapiRef.setRowGroupingModeland the assistant change. A grid that passesrowGroupingModelstays controlled. (v2.0 removed this callback because nothing called it; the assistant andapiRefnow do.) - New types:
GridAiAssistantOptions,GridAiAssistantPanelProps,GridAiAssistantStatus,GridAiChip,GridAiPromptContext,GridAiPromptResult,GridAiHistoryEntry,GridAiModelRequest,GridAiPromptHandlerOptions,GridAgentTool,GridAgentToolResult,GridAgentToolsOptions,GridAgentApi(exported from/ai);GridToolbarProps.onAiAssistantToggle/aiAssistantOpen/aiAssistantLabel;GridLocaleText.aiAssistantButton. - Demo page AI Assistant (
/ai-assistant): a simulated, rule-based model (no API key, no network), suggestion chips, the realfetchintegration sample, and a scripted agent driving the grid throughcreateGridAgentTools. Package smoke suite:ai-assistantscenario in both fixtures. -
Header filter row —
headerFilters(default off) adds a filter row under the column headers, inside the sticky header: a text box forstring, a number box fornumberand a date box fordate(the sharedInput), an Any / Yes / No select forbooleanand avalueOptionsselect forsingleSelect(none forimage), each with an operator button that opens a menu of the type's operators and a clear button. Typing is debounced by 300 ms; typing still pending is committed if the column scrolls out of the render window. NewGridColDef.headerFilter(false: empty cell) andGridColDef.headerFilterOperator(starting operator; defaultscontains,=,is), andheaderFilterHeight(default 40, CSS variable--ogx-header-filter-height).-
One shared
filterModel. Each cell owns only the root-level itemid: 'header:<field>': it is added at the end, updated in place and removed when cleared, and every other item, group,logicOperatorandquickFilterValuespasses through. The filter panel edits the same item and keeps its id. When the rootlogicOperatoris'or', the field sits inside a group, or the field has an untagged or second root item, the cell shows a read-only "Custom filter" label instead (a button that opens theGridToolbarfilter panel); the row never rewrites a model it cannot show. WithfilterMode="server"changes are reported throughonFilterModelChangeas usual. Not rendered whilepivotModeis active. -
Layout. Filter cells take each column's width, flex and sticky offset from the same calculation as the column headers, so they line up with pinned columns, column virtualization and horizontal scrolling. Scroll-into-view (keyboard,
scrollToIndexes) and range auto-scroll measure the sticky header, so rows never end up under the taller header. -
Keyboard and ARIA. The row is a header row (
role="row", counted inaria-rowcountand the rows'aria-rowindex) ofrole="columnheader"cells labelled "Filter <header>". ArrowDown on a column header moves to its filter cell and on into the first row; Enter, Space or typing focuses the control, Escape returns to the cell, Alt+ArrowDown or Ctrl/Cmd+Enter opens the operator menu, Delete clears the filter. Controls havetabIndex={-1}: the grid stays a single Tab stop and Tab from a control leaves the grid. - Public classes
ogx__header-filter-row,ogx__header-filter-cell(--focused,--active,--custom,--empty,--system,--pinned,--pinned-left,--pinned-right,--pinned-left-last,--pinned-right-first),ogx__header-filter-operator(--open),ogx__header-filter-menu,ogx__header-filter-input-wrapper,ogx__header-filter-input,ogx__header-filter-select,ogx__header-filter-operator-label,ogx__header-filter-clear,ogx__header-filter-custom. The row uses the header's theme variables, so every built-in theme and dark mode style it. -
GridToolbarProps.forceFiltersOpen/onFiltersPanelClose(the grid passes them so "Custom filter" can open the panel);HeaderProps.headerFilters,filterModel,onFilterModelChange,onOpenFilterPanel, andfocusedCell.headerFilter. - Guide
docs/features/header-filters.md, demo page Header Filters (/header-filters), and aheader-filtersscenario in both package smoke fixtures.
-
One shared
-
Fill handle — with
cellSelectionon, the range's bottom-right cell (or the only selected cell) carries a fill handle. Drag it down, up, right or left (one direction: the side the pointer's cell lies beyond, the larger pointer movement on a diagonal; auto-scrolls past the grid edge like a range drag; touch drags work,touch-action: none) to fill the new cells from the range: numbers and dates with an exactly equal step continue as a series (decimals rounded to the source's places; dates by month on the same day of the month, clamped to the month end, else by calendar day), anything else repeats the source pattern, one cell copies. Alt at release copies instead of continuing; Escape cancels. Ctrl/Cmd+D fills down from the range's top row and Ctrl/Cmd+R right from its left column (focus in the grid, no editor open; the browser's bookmark and reload keys are prevented only then). Fills write throughvalueSetterandprocessRowUpdateonce per row as one batch edit and one undo step; non-editable, synthetic and span-covered cells are skipped ('notEditable'); the range becomes the source plus the filled cells. New propsdisableFillHandleandonFill(result)(GridBatchEditResultplusdirection), newGridColDef.fillValue(params)to decide a column's filled values (a throw skips the cell,'invalidValue'), new typesGridFillDirection,GridFillResult,GridFillValueParams. Public classogx__cell-fill-handle(24 px hit area), variable--ogx-fill-handle-color(defaults to the range border; theme keygrid.fillHandleColor, set by every built-in theme). Guide:docs/features/fill-handle.md. Demo page Fill Handle (/fill-handle). - Package smoke suite:
fillscenario (handle drag series, Ctrl/Cmd+D, undo) in both fixtures.
-
GridAiState,GridAiPartandGridAiValidationErrorare declared in the grid's types so theaiAssistantprop can use them;@opencorestack/opengridx/aire-exports them, so existing imports keep working. - The
/aisize budget incheck-bundleis 8 kB gzipped (was 5 kB); the entry point is about 6.7 kB with the handler and tools.
-
Inputforwardsrefto its<input>(React.forwardRef), sorefworks on React 18 as well as 19 (before, it only reached the<input>on React 19, through the props spread).Input.displayNameis'Input'. -
Inputvariant—'field'(default, unchanged look) or'cell': the wrapper fills its container (width and height 100%,display: flex) with no border, radius, background or focus shadow, and the<input>uses the cell's font and the editor padding. For custom cell editors inrenderEditCell; adornments anderror(an inset--ogx-color-erroredge, drawn over the editing cell's outline) still work. New public classogx-input-wrapper--cell. An editing cell that holds one stretches its content to the full cell height. -
InputinputClassName— classes for the<input>itself;classNamestill goes to the wrapper<div>.errornow also setsaria-invalid="true"on the<input>. -
Checkboxforwardsrefto its<input>(alongsideinputRef), and takesinputClassNamefor the<input>;onMouseDown,tabIndex,aria-*and other props reach the<input>as before. - Demo page Custom Cell Editors (
/custom-editors): price and weight editors built on<Input variant="cell">with a$/kgadornment and an error state; guide section "Building custom editors withInput" indocs/features/editing-reordering.md. -
Clipboard paste — with
cellSelectionon, Ctrl/Cmd+V pastes tab-separated text (Excel, Google Sheets) into the editable cells: one value fills a range, a block extends from the range's top-left cell (or the focused cell) and is cut to the range and the grid edges; no rows are created. Thepasteevent is read, so no clipboard permission is asked. Each cell is parsed for its column type (1,234.5,12%, currency symbols, ISO andDate.parsedates keeping the cell's kind,yes/no/1/0,singleSelectvalues or labels; empty →null,''for strings) and written throughvalueSetterandprocessRowUpdateonce per row. Non-editable cells, synthetic rows and span-covered cells are skipped ('notEditable'), text that does not parse too ('invalidValue'). The pasted area becomes the selection. New propsdisableClipboardPaste,onClipboardPaste(result)({ updated, failed, skipped, text }) andonBeforeClipboardPaste({ text, anchor })(returnfalseto cancel or a string to replace the text); newapiRef.pasteText(text, anchor?); development warning above 10,000 cells. Guide:docs/features/clipboard.md. Demo page Paste from Excel (/clipboard-paste). -
GridColDef.valueParser(text, { row, field, colDef })— replaces the paste parser for one column (decimal-comma locales, custom codes); a throw orundefinedskips the cell. -
Clear a range with Delete — with
cellSelection, Delete / Backspace on a range of more than one cell empties its editable cells (''for strings,nullotherwise) as one edit.disableRangeClearturns it off. -
Undo / redo —
undoRedo(boolean | { limit }, default off, 100 actions) records committed edits, pastes and range clears (only afterprocessRowUpdatesucceeded, only the fields the user changed) and replays them throughvalueSetterandprocessRowUpdate. Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z and Ctrl+Y while focus is on a cell and no editor is open; the affected cells become the selection. A cell that changed since it was recorded is skipped ('changed', values compared, not row objects), a removed row too ('missing'). NewonHistoryChange({ canUndo, canRedo, size })andapiRef.undo(),redo(),canUndo(),canRedo(),clearHistory(). Guide:docs/features/undo-redo.md. Demo page Undo & Redo (/undo-redo). -
Batch edits — paste, range clear, undo and redo commit through one internal path (
useGridBatchEdit): oneprocessRowUpdateper row, rows in parallel, a failing row reported toonProcessRowUpdateErrorwithout blocking the others, and every successful row stored in one state update (a 1,000-row paste is one re-render). Result typeGridBatchEditResult({ updated, failed, skipped }) withGridEditSkipReason. New public types:GridValueParserParams,GridEditSkipReason,GridBatchEditResult,GridClipboardPasteResult,GridBeforeClipboardPasteParams,GridHistoryChangeParams,GridUndoRedoOptions. - Package smoke suite:
pastescenario (paste, undo, redo with the real clipboard) in both fixtures. -
AI toolkit, part 1:
@opencorestack/opengridx/ai— a new entry point (dist/ai.es.js,dist/ai.cjs,dist/ai.d.ts; about 4.5 kB gzipped) with no React import and no dependencies; the core bundle does not contain it, and no AI service or SDK is involved.-
getGridAiSchema(columns, options?)returns a JSON Schema (draft 2020-12,schemaVersion: 1) of thefilterModel(nested AND/OR groups up to 3 levels,quickFilterValues),sortModel,rowGroupingModel,aggregationModel,pivotModelandcolumnVisibilityModelthe columns allow: fields fromfilterable/sortable/groupable/hideable/aggregable, operators per column type, functions fromavailableAggregationFunctions,valueOptionsas enums.options.include/excludechoose parts;options.descriptions(default on) addsheaderNameanddescription. No row data is read; the output is deterministic. -
validateGridAiState(json, columns, options?)returns{ state, errors }: unknown parts, fields, operators and enum values are dropped with{ path, message }errors, values are coerced to the column type ("1,200"→ 1200,"12%"→ 0.12, yes/no → booleans, dates →YYYY-MM-DD, singleSelect by value or label), empty filter groups are removed. Accepts an object or a JSON string and never throws. -
GridColDef.aiExamples— opt-in example values for the schema. They are sent to the model, so list only data you are happy to share. - Types
GridAiColumn,GridAiPart,GridAiPartOptions,GridAiSchema,GridAiSchemaOptions,GridAiJsonSchema,GridAiState,GridAiValidateOptions,GridAiValidationError,GridAiValidationResult, exported from/ai. - Demo page AI: Grid Schema & Validator (
/ai-schema) and guidedocs/features/ai-toolkit.md.check-bundleenforces the 5 kB budget and that the ai bundles import nothing; the package smoke suite imports/aiin both fixtures.
-
- The toolbar's global search, the column panel's search box, the filter panel's text/date value box and the built-in text, number and date cell editors render through
Input, so their<input>now sits inside adiv.ogx-input-wrapperand also has the classogx-input. Selectors with a child combinator such as.ogx-global-search > inputor.ogx-filter__controls > inputmust become descendant selectors (.ogx-global-search input). The class names on the inputs (ogx-global-search__input,ogx-column-visibility-panel__search-input,ogx-filter__value-input,ogx__edit-input,ogx__edit-input--number), their ids, names and ARIA attributes are unchanged, and they look the same. - The boolean cell editor and the list view's row checkbox are the shared
Checkbox(the drawn box used by the row-selection column) instead of a native checkbox. The editor's<input>keepsogx__edit-checkboxbut is visually hidden; click the box (.ogx-checkbox__box). The Safari fix from 3.2.1 is kept: mousedown anywhere in the boolean editor is prevented, so focus stays in the editor until the click toggles it. The.ogx-list-view__checkbox input[type="checkbox"]size /accent-colorrule is gone. -
Input.cssnow loads before the component styles inopengridx.css(it was afterColumnVisibilityPanel.cssandGridEditInputCell.css), so component rules that restyleInputwin at equal specificity. Nothing else in the library's CSS styled.ogx-input*. - The built-in text, number and date editors now fill the editing cell's full height (they were as tall as their text); the look is unchanged.
-
GridApihas six new members (pasteText,undo,redo,canUndo,canRedo,clearHistory); an object written to satisfyGridApiby hand must add them.useGridApiRef()'s placeholder has no-op versions. - A committed single-cell edit is stored through the same path as batch edits; behaviour is unchanged.
- Cell range selection: a drag that auto-scrolled could end short of the pointer (since 3.3.0). Dragging past the bottom of the grid scrolls it; on a slow machine the rows scrolled into view were not rendered yet when the pointer was hit-tested, so the range's head stayed on an earlier row (and the release could leave it there). When no cell is under the pointer during a drag, the head row is now worked out from the scroll position and the row heights.
-
Wrapped header text — new grid prop
wrapHeaderTextand per-columnGridColDef.wrapHeaderText(the column wins). Long header titles wrap instead of being cut with an ellipsis, within the existingheaderHeight: 2 lines at 56px, 3 at 72px, 4 at 88px, with an ellipsis on the last line if the title is still longer. The header never grows on its own; raiseheaderHeightfor more lines. Wrapped header cells get the new classogx__header-cell--wrapand show their full title as thetitletooltip unless the column has adescription. Sort icon, column menu button and resize handle are unchanged, and column auto-size still fits the title on one line. Demo: Wrapped Header Text (/header-wrap). -
Cell range selection (
cellSelection) — select a rectangle of cells as in Excel or Google Sheets: press and drag (the grid auto-scrolls over virtualized rows and columns when the pointer nears or leaves an edge), Shift+click, Shift+Arrow (no row wrap), Shift+Home/End, Ctrl+Shift+Home/End, Shift+PageUp/PageDown, Ctrl/Cmd+A (every data cell of the page) and Escape (collapse to the anchor). Ctrl/Cmd+C copies the range (the focused cell at minimum) as TSV, formatted as shown, without a header line;apiRef.current.copySelectedRows()still copies rows. Ranges never change the row selection; the checkbox, detail-panel, reorder and__group__columns never join a range. Group rows, subtotals, tree auto-parents and the pivot Grand Total are highlighted but not copied. A range touching part of acolSpan/rowSpangrows to cover it. Off by default: grids without the prop are unchanged. -
cellSelectionModel/onCellSelectionModelChange(model, { reason })— controlled or uncontrolled model of{ anchor, head }corners addressed by row id and field, so a range follows its rows through sorting; it is cleared (reason: 'dataChange') when a corner is no longer displayed. New typesGridCellCoordinates,GridCellRange,GridCellSelectionModel,GridCellSelectionReason,GridCellSelectionChangeDetails,GridSelectedCell. -
GridApi:getCellSelectionModel,setCellSelectionModel,selectCellRange,clearCellSelection,getSelectedCells,copySelectedCells. -
showCellSelectionStats— a status bar with the Count, Sum and Average of the range (shared aggregation functions), replaceable throughslots.cellSelectionStats(GridCellSelectionStatsSlotProps); the default component is exported asCellSelectionStats. -
Accessibility — range cells carry
aria-selected="true", focus stays on the anchor, and the live region announces the size ("12 cells selected, 3 rows by 4 columns", debounced 300 ms); translate it withlocaleText.cellSelectionAnnouncement. -
Styling — public classes
ogx__cell--range,ogx__cell--range-top|bottom|left|right(inset outline, no layout shift) andogx--range-dragging; CSS variables--ogx-range-backgroundand--ogx-range-border, defined in every built-in theme and settable with the theme keysgrid.rangeBackground/grid.rangeBorder. - Demo page Cell Range Selection and guide
docs/features/cell-selection.md.
-
Rowis now memoised (itscellRangeprop compared by value), and the row click / checkbox handlers keep their identity across renders, so rows whose props did not change no longer re-render.
-
React Compiler compatibility checked — the library works in apps that use React Compiler, and when
lib/itself is compiled (unit and browser suites pass withREACT_COMPILER=1). Fixed what the compiler rejected: a prop mutated inCheckbox, a self-referencing callback inuseGridEditing, memoization the compiler could not keep inFilterPanel/GridToolbar, value expressions insidetryinCell/useAggregation/useGridEditing, andGridToolbar'sforceColumnsOpeneffect.InlineToolbar(slots.toolbar) andCellRenderTarget(renderCell) are marked'use no memo': compiled, they skipped the consumer's toolbar / cell function on re-render and broke its hooks. ESLint usesreact-hooksrecommended-latest(7.1.1) withimmutabilityandpreserve-manual-memoizationon; newnpm run check:compiler. Findings, remaining issues and how to re-run:docs/contributing/react-compiler.md. -
GitHub wiki generated from
docs/—scripts/build-wiki.mjs(npm run wiki:build) turns the docs, CHANGELOG and CONTRIBUTING into wiki pages with a sidebar, rewrites relative links to wiki pages (or GitHub URLs for source files and images), and adds hand-written Home, Getting Started and FAQ pages fromwiki/..github/workflows/wiki.ymlpublishes it on every push tomainthat touches them.
- Exports no longer slow down on crafted cell text — the check that tells plain signed numbers ("-12.50") from spreadsheet formulas (CSV/Excel formula-injection guard) could backtrack quadratically on long runs of digits, spaces and separators that end in a non-number character, freezing CSV or Excel export of such a value. It now runs in linear time; results are unchanged. Found by CodeQL.
-
Sheet names and theme heights — stripping apostrophes from Excel sheet names and parsing
'36px'-style theme heights used patterns with the same quadratic worst case; both are linear now, results unchanged. -
Column auto-size selector fallback — where
CSS.escapeis unavailable, field names are now escaped for backslashes as well as quotes.
-
exceljsis a devDependency (it stays an optional peer for consumers), so a regenerated lockfile keeps it. - Dev-only dependency alerts:
undici7.30.0,dompurify3.4.16, anduuidoverridden to^11.1.1(pulled in byexceljs4.4.0) in the repo and smoke fixtures. Consumers who installexceljsthemselves still get itsuuid8 untilexceljsupdates it.
- Boolean editor in Safari — clicking the edit checkbox closed the editor without toggling the value. Safari does not focus a checkbox on click, so mousedown moved focus to the cell and the blur committed the unchanged value before the click arrived. The checkbox now keeps focus on mousedown. Found by the new package smoke suite (WebKit).
-
Community files —
CODE_OF_CONDUCT.md(Contributor Covenant 2.1),CONTRIBUTING.md, GitHub issue forms (bug report, feature request) and a pull request template. -
Package smoke suite (
npm run test:smoke) — packs the library and installs the tarball into strict-TypeScript React 19 and React 18 apps (e2e/fixtures/), builds them and runs Playwright Test in Chromium, Firefox and WebKit: rendering, sorting, quick filter, selection, pagination, a 50,000-row grid filling a flex app shell without aheightprop, row grouping with aggregation, CSV / XLSX / PDF downloads, editors, pinning, column groups, detail panels and the dark theme. Runs in the newci.ymlworkflow (pull requests), beforenpm publish, and weekly against the newest dependency versions. Seedocs/contributing/testing.md.
-
Non-blocking
exportToPdf— the export now works in ~30 ms slices and yields to the event loop between them, both while formatting rows (including grouped rows and WinAnsi conversion) and while drawing the table: the body is drawn by a series ofjspdf-autotablecalls that continue on the same page with fixed column widths, the header repeated on every new page and the footer only at the end. Pages, row positions, shading and page breaks are unchanged (verified against a single call in Chromium, Firefox and WebKit). Measured with 50,000 grouped rows in Chromium: longest main-thread block 6.3 s → ~0.4–0.6 s (jsPDF's final save, which cannot be split), total time about the same (~6–7 s), file size unchanged. -
PdfExportOptions.onProgress—({ phase: 'prepare' | 'render' | 'save', done, total }) => void; new exported typePdfExportProgress. -
PdfExportOptions.signal— anAbortSignal; aborting rejects the promise with aDOMExceptionnamed'AbortError'and saves no file. -
PdfExportOptions.maxRows(default 20,000) — above it a development-modeconsole.warnrecommendsexportToCsv/exportToExcelAdvanced. The export still runs; nothing is thrown. - Demo: the PDF export page has a row-count selector, a progress bar and a Cancel button.
-
GridColDef.sortComparator(v1, v2, params1, params2)— a per-column ascending comparator for client-side sorting, used instead of the built-in type-aware comparison.v1/v2are the cell values aftervalueGetter, includingnull/undefined(the comparator decides where empty values go);paramsare the new exportedGridSortCellParams{ id, field, row, value }. The grid negates the result fordesc. It is used for flat sorting, as one key in a multi-sort chain, to order row-grouping groups by their grouping value (the grouping column's comparator), for tree-data siblings and for pivot row-label columns, sogetAllFilteredRows(), exports and clipboard follow the same order. A comparator that throws or returnsNaNreads as0with a one-time dev warning. Ignored withsortingMode="server". -
Auto-size a column to its content — double-click a column's resize handle (or press Enter with the handle focused) to fit the column to its header and the cells in the current render window (pinned rows included), measured in the DOM and clamped to
minWidth(default 50px) /maxWidth. A flex column gets a fixed width, like after a drag;resizable: falsecolumns are never auto-sized; the double-click never sorts. Works for left/right-pinned columns and under column groups. The width goes tocolumnWidthsstate, soonStateChangereports it. -
apiRef.current.autosizeColumn(field)andapiRef.current.autosizeColumns(fields?)— the same measurement, programmatically (every column whenfieldsis omitted).
- Double-clicking a resize handle used to do nothing; it now auto-sizes the column.
Fixes from a smoke test in a real application (Chromium, Firefox and WebKit).
-
A grid without a
heightprop now fills its container. Its root was auto-height, so inside a bounded flex layout (a wrapper withflex: 1; min-height: 0, with or withoutDataGridThemeProvider) it grew to fit every row and rendered all of them; 50,000 rows froze the tab. The root now getsheight: 100%and, as a flex item,flex: 1 1 auto; min-height: 0(classogx--fill) when neitherheight,style.heightnorautoHeightis set. In an auto-height container it still grows to fit, as before. An explicitheightis never stretched. -
The unbounded-container development warning is only logged when the viewport is at least as tall as the rows it renders, and names the fixes: a definite container height,
min-height: 0on every flex or grid ancestor up to the sized one, aheightprop, orautoHeight. - The list view warns in development when it renders more than 2,000 items. It does not virtualize; README and the list-view guide now say so and recommend pagination.
-
The whole column resize handle can be grabbed. The 8px handle straddled the header cell border and the cell's
overflow: hiddenclipped half of it, leaving 3–4px in every engine. It now lies inside its column against the resizing edge (also in the column-group header layout and for right-pinned columns). - Right-pinned columns sit at the right edge when the columns are narrower than the grid, in the header, column-group rows, body, pinned rows and aggregation footer. They used to follow the last unpinned column, leaving a gap on their right.
-
List view, Firefox: the rows container was a Tab stop of its own (Firefox makes scroll containers focusable), so Tab landed on it instead of the focused row. It now has
tabIndex={-1}.
-
Typed
apiRefrow getters.GridApi<R>(defaultGridRowModel) anduseGridApiRef<R>()typegetRow,getAllRows,getVisibleRowsandgetAllFilteredRowsas your row type.DataGridProps.apiRefaccepts a typed or an untyped ref, so existing code compiles unchanged.
- New root class
ogx--fill(height: 100%; flex: 1 1 auto; min-height: 0) when noheight,style.heightorautoHeightis set. CSS that sets the grid's height on.ogxstill wins only if it is more specific or passed asstyle.height/height. -
.ogx-column-resize-handleisright: 0(was-4px) withjustify-content: flex-end;.ogx-column-resize-handle--startisleft: 0(was-4px) withflex-start. The visible line stays on the column border. -
.ogx__header-cell--pinned-right-first,.ogx__cell--pinned-right-first,.ogx__aggregation-cell--pinned-right-firstand the first right-pinned column-group cell getmargin-left: auto; with right-pinned columns,.ogx__contentismax(100%, <total>px)wide instead of the column total. -
.ogx-list-view__rowshastabIndex="-1".
- The browser test suite and CI (deploy and publish workflows) run in Chromium, Firefox and WebKit.
- README
heightrow, Getting Started, virtualization, theming, datagrid and pinning docs describe how the grid fills its container. - PDF export: the
fontoption is shown up front with a₹example, and the size and time of very large exports are documented (50,000 grouped rows ≈ 120 MB, 10–16 s on the main thread); use CSV or.xlsxfor large exports. - README: a rejected
processRowUpdatekeeps the cell in edit mode with the typed value and callsonProcessRowUpdateError.
A correctness release: every feature area was audited against its documentation and the grid's own behaviour. Most changes are bug fixes, but many of them change observable output (formatted text, exported files, callback indices, request ranges), which is why this is a major version.
Upgrading from 2.x: read docs/migration/v2-to-v3.md. It starts with a table that maps what your app uses to the sections you need.
React 18 users: 2.1.0 crashed on React 18 (the build bundled React 19's JSX runtime). 3.0.0 works on React 18 and 19 again; see Fixed → Build & SSR.
-
The underscore hierarchy fields are no longer added to rows.
_hasChildren,_treeDepth,_isExpanded,_groupingField,_groupingValue,_descendantCountand_isGroupRow(deprecated since 1.1) are gone at runtime; useparams.rowMeta. TypeScript does not flag reads of them, becauseGridRowModelhas an index signature. -
params.rowunder row grouping and tree data is now the consumer's own row object, not a per-render copy. Code that mutatedparams.rowin a render callback now mutates the source data. As a side effect, rows are no longer re-created on every render. -
getRowIdno longer writes anidonto your rows. Rows are stored untouched everywhere (grid, callbacks,processRowUpdate,apiRef), sorow.id === getRowId(row)no longer holds. PassgetRowIdto export functions when exportingselectedRows. -
Grouped-export default labels in
exportToExcelAdvancednow match the grid. Without agroupingValueFormatter, group headers read"field: value"(the grid's documented default) instead of"Header: value". SetgroupingValueFormatteron the grouping column to control the label in the grid and in every export format. -
Cells are formatted by column
typewhen there is novalueFormatter:dateshowstoLocaleDateString(),booleanshows Yes/No,singleSelectshows the option label andimagerenders an<img>. This applies to cells,params.formattedValue, list view and text exports. - Empty filter values no longer filter, the quick filter searches only visible filterable columns, and the toolbar search box splits its text into words that must all match.
-
Aggregation ignores blank strings, booleans and arrays in
sum/avg/min/max, andmin/maxover dates return aDate(was epoch milliseconds). -
CSV files start with a UTF-8 BOM and text that looks like a formula gets a leading
'(CSV and basic Excel). Opt out withbom: false/escapeFormulas: false. -
A non-empty
selectedRowsnow wins overgroupedRowsin every export: the selection is exported flat and totals are recomputed over it. -
exportToExcelalways writes.xls; a.xlsxfile name is renamed with a warning (the file was always HTML, which Excel rejects under.xlsx). - Tab leaves the grid in one press outside edit mode (it used to step through cells). While editing, Tab still moves to the next editable cell.
-
Index values changed meaning:
colIndexis absolute among visible data columns (was window-relative), bottom-pinned rows getrowIndexafter the page rows (were 0..n),onRowOrderChangeindices are positions in therowsprop (were page-local and sorted), andonColumnOrderChangeindices are positions in the full column order. -
isCellEditablenow governs every way of starting an edit (double-click, Enter, Tab) andaria-readonly, not only Tab stops. -
processRowUpdateis called when an edit is lost (the cell scrolls out, is filtered or paged away, or another cell starts editing). Those edits used to be dropped. -
Built-in editors commit the column's value type:
singleSelectcommits the option value (not its string form),datecommits aDateforDatecells andnullwhen cleared. -
Pinned columns render in
pinnedColumns.left/.rightorder, not column order. -
Header and top-pinned rows are wrapped in
div.ogx__sticky-top, bottom-pinned rows and the aggregation footer indiv.ogx__sticky-bottom..ogx__pinned-rows--top/--bottomare no longer sticky themselves; custom CSS that targets them must move to the wrappers. - Group and pivot row ids changed format. Row-grouping ids for non-string values, tree-data auto-parent ids and pivot row ids use new, collision-free formats; do not parse them.
-
DataGridThemeProviderpins a complete light or dark palette chosen by the newGridTheme.mode, whatever the operating-system colour scheme. A provider-wrapped grid no longer switches with the OS. -
Grid buttons have
type="button"(including the exportedButton), so they never submit an enclosing<form>. Passtype="submit"explicitly where you want a submit button. -
apiRef.current.copySelectedRows()rejects when the clipboard write fails (it resolved and logged). -
dataSourcerequests changed:getRowsis now called with all-client modes, infinite scroll requests the gap after the loaded rows, and tree children are requested withendRow: Number.MAX_SAFE_INTEGER(was-1). -
Column resizing uses pointer events (tests that simulate
mousedown/mousemovemust use pointer events) and no longer caps widths at 1000px. -
Removed
GridThemeSkeleton.darkBaseColoranddarkHighlightColor, which had no effect.
-
printGridescapes the title, cell values, image URLs and alt text, and error messages in the same-origin print window, and only renders images fromhttp(s),data:image,blob:and relative URLs. -
exportToCsvandexportToExcelneutralise spreadsheet formula injection: text starting with=,+,-,@, tab or carriage return gets a leading'. New optionescapeFormulas(defaulttrue). -
exportToExcelAdvancedwrites object values as text, so{ formula },{ hyperlink },{ richText }or{ error }objects in row data can no longer become live cells or links. - Grouped
exportToJsonno longer leaksexportable: falsecolumns through subtotals and the grand total.
- Grouped CSV, basic Excel, JSON, print and PDF exports ignored
groupingValueFormatterfor group-header labels; all exporters now share one label rule, andgetGroupedExportRows()entries carry the grid's label asgroupLabel. -
exportToExcelAdvanceddates were shifted by the UTC offset (a day early east of UTC). - NaN, Infinity and Invalid Date values produced a corrupt xlsx file.
-
selectedRowswas ignored whengroupedRowswas also passed; totals are now recomputed over the selection. - Exporting a large selection froze the tab.
- CSV did not quote values containing the configured delimiter or a bare carriage return.
-
count/uniquetotals were formatted as currency or dates. - A
valueFormatterthat reads other row fields or expects aDateaborted exports with totals. - The first column's aggregate was replaced by the Subtotal / Grand Total / TOTAL label; the label is now prefixed to the value.
- Spacer columns (
isSpacer) and grid system columns (__checkbox_col__,__expand_col__,__reorder_col__,__group__) passed incolumnswere exported; they are always left out. - A
valueFormatterplaceholder for missing values is exported, as the grid shows it. - Basic Excel dropped leading zeros and mangled long numeric ids, and wrote invalid sheet names.
-
exportToExcelAdvanced:rows: 'selected'sheets contain only the selection; invalid or duplicate sheet names no longer throw;embedImagefollowsvalueGetterand falls back to the URL for SVG, WebP and AVIF; ISO date strings and numeric strings become native cells; integers have no trailing "."; totals and the summary sheet are numeric; striped rows have no gaps. - PDF: non-Latin-1 text was garbled wholesale;
#fffshorthand colours work; the row count is correct whengroupedRowsis[]; the filter summary reflects nested groups, OR and quick search, and wraps. - JSON aggregation values are numbers, as documented.
- Print keeps group-row highlights on even rows.
- Date operators
after,onOrAfter,beforeandonOrBeforedid not filter (every row passed, with a warning per row). - Date
is/notcompared raw strings; they now match the local calendar day forDatevalues, ISO datetimes and epoch numbers. - A filter item with no value filtered anyway (
>meant> 0,=hid every row). - Numeric filters treated blank cells as 0;
!=treats empty cells consistently. - A
singleSelectfilter set from the panel emptied the grid; the panel now offers the column'svalueOptions. - Opening the filter panel rewrote values (an
isAnyOfarray became"a,b") and firedonFilterModelChange. - Editing a panel row deleted filter groups and other conditions on the same column.
- Changing the operator right after typing lost the typed value, and a pending keystroke undid Clear all.
-
valueGettercolumns could not be sorted, filtered or quick-searched (including under tree data and row grouping). - The quick filter matched the row id, hidden columns, non-column fields,
[object Object]andDate.toString()text; it now matches what cells show. - A multi-word toolbar search now matches words across columns.
- The row-grouping "Group" column appeared in the filter panel.
- Number columns holding numeric strings sort numerically; NaN and Invalid Date no longer break sorting; accented strings sort next to their base letter.
- Shift-click on a sorted column kept moving it to the end of the sort; column-menu Unsort / Asc / Desc cleared the other sort keys.
- The toolbar search box stole focus back on re-render.
- The boolean filter could not select "true" directly; the panel shows the real operator of programmatic items.
-
initialState.filterwas ignored; the toolbar search and Filters button did nothing without afilterModelprop. - A
valueGetterorvalueFormatterthat throws no longer crashes the grid once the column is sorted, filtered, quick-filtered, aggregated, grouped or pivoted, or shown in list view: the value reads asundefinedand a dev warning is logged once per column. - Text typed into the FilterPanel or the toolbar search box just before the panel closes is no longer lost.
-
isCellEditabledid not block double-click and Enter editing; rejected cells now carryaria-readonly="true". -
processRowUpdateran twice for one commit (Tab, or Enter then blur). - A slow
processRowUpdateclosed an edit on another cell or discarded text typed meanwhile. - Clicking inside an open editor committed and closed it; the dropdown editor was unusable with the mouse; clicks inside editors fired
onCellClick/onRowClickand toggled selection. - An edit was lost when its row scrolled out, was filtered or paged away, or another cell started editing; it is now committed.
- Committing one edit reverted other changes to the row and reset the server row count.
- Tree-data parent rows could not be edited by double-click; Enter opened a useless editor on row-grouping group rows.
- Committing by clicking a control outside the grid stole focus back.
- The
singleSelecteditor committed typed option values as strings and showed no empty choice for an empty or unknown value. - The date editor was blank for
Dateand ISO datetime values and committed a different kind of value; clearing it commitsnull. - With a custom
getRowId, aprocessRowUpdateresult without the grid's id was not applied. - An inline
getRowIdfunction reverted committed edits on every parent re-render. -
onRowDoubleClickdid not fire on editable cells. - The boolean editor did not close on blur; IME composition Enter committed the edit; built-in editors had no accessible name.
- Double-clicking inside an editor discarded the typed value.
- A
processRowUpdatethat returns nothing is now reported clearly and the editor stays open. - Enter-to-edit starts from the
valueGettervalue. - Every cell re-rendered on every grid render.
-
apiRefmethodssortColumn,setFilterModel,setPage,setPageSize,selectRowandselectRowsdid not change the grid or fire callbacks;getSortModel,getFilterModelandgetSelectedRowsreturned stale values. -
apiRef.current.getSelectedRows()returned the previous selection insideonRowSelectionModelChange. -
getVisibleColumnsincluded hidden columns and ignored display order;getVisibleRowsandgetAllFilteredRowsomitted pinned rows;getAllFilteredRowsmissed rows under tree data and row grouping. -
getGroupedExportRows()dropped the rows of collapsed groups and ignored sort and filter. - Select-all selected filtered-out rows, could not be undone with a
dataSource, and the header checkbox showed the wrong state. -
disableMultipleRowSelectionwas ignored by checkboxes, Space and select-all. - Selection kept the ids of rows that were removed from
rows. -
getRowIdoverwrote the row's ownidfield (grid,dataSourcerows and tree children). - Duplicate row ids rendered one row twice.
-
onStateChangelooped with inline models; its payload now carriesdensityand never contains__group__;initialState.densityis honoured. -
useGridStateStoragecrashed when storage is blocked,clearStatewas undone, and storage was written on every re-render. -
useGridApiRef()did not type-check against theapiRefprop under@types/react18. - Changing
rowsandcolumnstogether (or leaving pivot mode) ran the new columns against the old rows, crashingvalueGetters. - A controlled
rowSelectionModelholding an id that is no longer inrowsfiredonRowSelectionModelChangeon every render and could loop; the pruned selection (and a corrected page) is now reported once, also under StrictMode. -
getAggregationResult,getAggregationModel,getGroupedExportRowsandscrollToIndexesare available in a parent'suseLayoutEffect. -
getVisibleColumnsreturns columns in render order (left-pinned, unpinned, right-pinned). -
apiRef.selectRow/selectRowsignore synthetic ids (group, subtotal, auto-parent and pivot Grand Total rows), and a call that does not change the selection fires nothing. The pivot Grand Total row cannot be selected.
- The pager total ignored the filter and counted pinned rows.
- Server pagination, sorting or filtering without a
dataSourcere-sliced, re-sorted or re-filtered the rows. - The
rowCountprop was read only at mount. - The default page size could be missing from
pageSizeOptions. -
slots.footerreceived the page length instead of the server total. - Shrinking data left the grid on an empty page past the end; it now shows the last page and reports it through
onPaginationModelChange. - The rows-per-page select did not show a page size missing from
pageSizeOptions, had a hard-coded accessible name, and showed "Infinity" for page size 0. - Infinite scroll skipped rows when the page advanced quickly or while loading; it now requests the missing range without duplicates.
- Infinite scroll: changing sort, filter,
pageSizeordataSourcerestarts from the first row and reports page 0. - Server sort or filter with client pagination showed only the first page.
- A
dataSourcewith the default (client) modes never loaded its rows. - The error overlay's Retry reloaded the whole page; it now re-requests rows, and shows the message of any rejected object with a
message. - Stale responses overwrote newer ones.
- Inline
filterModel,sortModel,aggregationModelordataSourceobjects refetched on every render and wiped loaded infinite pages. - An inline
rows={[]}next to adataSourcewiped the fetched rows. - "No Data" flashed before the first response, and the live region announced "Loading data... No Data".
- Server tree data: children now load with every row, keep the pager total, are not re-filtered or re-sorted under server modes, reload for expanded nodes after a refetch, and a failed children request collapses its node instead of covering the grid.
-
paginationMode="infinite"combined withpaginationsliced the rows. -
loadingwith rows present showed nothing; it now shows a progress bar (orslots.loadingOverlay) over the rows. - A
dataSourceresponse that cannot be processed shows the error overlay instead of loading forever. - Row grouping with a
paginationMode="server"dataSourceloads every row in one request (with a dev warning) instead of grouping only the first page.
- The aggregation footer double-counted expanded groups. Under row grouping it aggregated the visible rows: group rows, which already carry their subtotals in the same fields, plus the expanded leaves. Expanding a group inflated
sum, andcount/avg/uniquewere wrong even when every group was collapsed. The footer,getAggregationResult(), theaggregationResult/rowCountpassed toslots.footer, and grouped-export grand totals now use the filtered data rows, independent of expansion. - Group rows lost their label and expand toggle when the first column was hidden, and showed them mid-row when that column was reordered or another column was pinned left; the hierarchy UI now goes on the leftmost column on screen.
- Subtotals, the "(n)" count and
descendantCountcounted filtered-out rows; groups left empty by the filter are hidden. -
getAggregationPositionwas ignored; it is now called for every group and once withnullfor the grand total. -
groupingColDefhad no effect under tree data. -
pinnedRowsdisappeared under tree data and row grouping. - Tree paths containing
/collided with deeper paths. - Grouping values
nulland'null',1and'1'fell into the same group. - Detail-panel callbacks ran for every rendered row, including group rows; they now run only for expanded data rows, and a throwing
getDetailPanelContentis contained to its panel. - A throwing
getDetailPanelHeight,groupingValueFormatterorgetAggregationPositionfalls back to the default instead of crashing. -
getGroupedExportRowsandgetAllFilteredRowsfollow screen order when sorting by the hierarchy column. -
defaultGroupingExpansionDepthexpands and loads lazy server tree nodes.
- Footer totals now use the column's
valueFormatterforsum/avg/min/max;count/uniquestay plain numbers, and datemin/maxshow as dates. - Group rows formatted counts with the column's currency or unit formatter.
- Blank cells, booleans and arrays counted as 0 in
sum/avg/min/max;count/uniquecounted blank strings. - Aggregates of
valueGettercolumns summedrow[field]instead of the computed values. -
aggregable: falsecolumns were aggregated whenaggregationModelorpivotModelnamed them. - Footer totals now come from the server whenever a
dataSourcedrives the rows, including infinite scroll and server-only filtering. - An inline
aggregationModelrefetchedgetRowsand cleared server totals on every render; stale server totals no longer show under a new function, filter or sort, while pending, or after a failure. - Pivot mode crashed with
RangeErroron datasets over ~110k rows with aminormaxvalue field, andcountcounted numeric values only. - Pivot: filters and the quick filter apply to source rows, filters on generated value columns apply to pivot rows, and the Grand Total matches the rows shown, stays last when sorting and is not selected by select-all.
- Pivot:
getRowIdcollapsed the pivot into one row;treeDataandrowGroupingModelcrashed or regrouped it; adataSourceproduced broken output (pivot is now ignored with a warning). - Pivot: the same field twice (for example
sumandcount) doubled values; a sourcevalueFormatterthat reads row data crashed the grid. - Pivot: row-label columns keep the source column's formatter, renderer, type and alignment; column-field headers are formatted; numeric column keys sort numerically.
- Pivot: an empty pivot keeps its value columns and shows the no-rows overlay; a row field named
idno longer overwrites row ids; pivot rows no longer share ids with source rows. - Pivot: leaving pivot mode restores the column order; a controlled
columnOrderno longer puts value columns before the label column, and the Columns panel can reorder generated columns. - Pivot:
getAggregationResult()andslots.footerno longer report totals over pivot rows, and the Summaries panel no longer lists generated columns. - PivotPanel: editing or removing one of two chips on the same field affected both; it now follows
aggregable: false,availableAggregationFunctionsandgroupable: false, and its controls have descriptive names.
- Columns pinned out of column order overlapped or left gaps; header, body, footer and keyboard navigation now agree on
pinnedColumnsorder. - Column widths ignored
minWidth/maxWidthfor fixed, resized and pinned columns; percentage widths cascaded; pinned%,flexandautocolumns were 100px wide. - Top-pinned rows covered the header when column groups were used; the aggregation footer was hidden behind bottom-pinned rows.
- Keyboard navigation and
scrollToIndexesleft the target row under the footer or bottom-pinned rows;scrollToIndexes({ colIndex })left the column under system or right-pinned columns. -
'auto'detail panels are laid out at their real height;getDetailPanelHeightreturning 0 renders 0px (was 200px). -
onRowsScrollEndfired on every scroll event near the bottom (including horizontal scrolling) and never for a list shorter than the viewport; it now fires once per arrival, in grid and list view. - The grid re-measures its viewport and restores the scroll position after list view is switched off.
- Infinite-scroll placeholder rows appeared before the last loaded row.
- Row-spanned cells painted over the sticky drag and expand columns.
- A new
overscanRowCountapplied only after scrolling. - Pinned-section edge classes and shadows were not applied.
-
autoHeightfiltered visible rows in quadratic time.
- Body cells lost
flex, percentage andautowidths (misaligned with headers) as soon as any column usedcolSpan. -
colSpan/rowSpanrecomputed on every resize tick (a ~2 s freeze per rows change at 100k rows) and cost time even when unused. - Spans counted hidden columns, crossed into another pinned section, and crossed between pinned and scrolling rows.
- A span broke when its origin row or column scrolled out of the render window.
- Huge, Infinity, NaN or fractional span values hung or corrupted the grid;
aria-colspan/aria-rowspanreport the clamped value. - A throwing
colSpan/rowSpancallback unmounted the grid; it now falls back to no span with a dev warning. Infinite-scroll placeholder rows are never passed to span callbacks. -
colSpanreceived the raw field value instead of thevalueGetterresult, and acolIndexdifferent fromrenderCell's. -
colSpanplusrowSpancovered only the origin column in following rows. -
rowSpanpainted over an expanded detail panel; a merged cell was capped by its origin column'smaxWidth. - Keyboard navigation lost focus on covered cells; arrows now cross a merged cell in one step.
- Column group header rows did not follow member columns under
flex/%/autowidths, hiding, reordering and pinning; pinned groups scrolled away; non-contiguous groups caused duplicate-key errors. -
GridColumnGroup.headerClassNamewas ignored. - Group header cells expose
aria-colspan/aria-colindex, filler cells are hidden from assistive technology, andaria-rowcountcounts group header rows. -
rowSpannever starts on or crosses group, subtotal, tree auto-parent or pivot Grand Total rows.
-
onRowOrderChangeindices depended on page, sort, filter and pinned rows; rows with id0and columns with field''could not be reordered. - With
rowReordering, pinned rows had no handle cell and misaligned; pinned and group rows could be dragged and dropped onto. - Row and column drags did not start in Firefox; drags from outside the grid were treated as reorders; a drag broke after its source row scrolled away.
- Header drag-reorder moved the wrong column after columns were added, when
__group__appeared, or with a partial controlledcolumnOrder; dropping on a pinned column sent the dragged column to the far end. -
pinnable: falsealso blocked drag-reorder. - Controlled or
initialStatecolumnOrderwas ignored whendisableColumnReorderwas set. - The Manage-columns panel listed columns in definition order, and its Reset did nothing under a controlled
columnOrder. - Resize clamped to a hidden 1000px maximum and snapped narrow columns to 50px; a click on the handle resized, sorted or turned a flex column fixed; ending a resize over the header sorted the column; right-pinned columns resized from the wrong edge; a resize kept running after unmount.
-
rowReorderinghad no effect but showed handles in pivot mode.
- Keys typed into inputs inside cells and detail panels were taken by the grid; clicking an input rendered by
renderCelllost focus. - Focus was lost when the focused cell scrolled out, on window switch, when tabbing out and back (the last cell is now restored), and when rows or columns were removed, filtered or hidden.
- Tab could not reach detail-panel content.
- Arrow keys stopped on hidden columns and ignored the pinned visual order; Space and arrows at the edges scrolled the viewport.
- ArrowDown onto a row with a tall detail panel scrolled the row out of view; Home onto an unpinned checkbox column did not scroll it into view.
- Enter / Space on the select-all header sorted by
__checkbox_col__instead of selecting all. - Keyboard sorting from a header ignored
multiSortand Shift. - Reorder, expand and checkbox columns were not real focus stops; pinned rows could not be edited.
- DOM focus did not return to the cell after a re-click or an edit.
- A row with id
"HEADER"was treated as the header row. -
aria-rowindex,aria-rowcount,aria-colindex,aria-colcountandaria-sortwere wrong;colIndexinonCellClick,renderCell,cellClassNameandrenderHeaderwas relative to the render window. - The detail panel had an invalid row/gridcell structure.
- A cell whose
renderCellthrew stayed broken; it now recovers when the renderer or value changes. -
GridTooltipplacementleft/rightdid not work; it now opens on focus, usesrole="tooltip"witharia-describedby, closes on Escape, uses theme colours and cleans up its timers. - Columns-panel checkboxes are announced with their column name.
- Copy (Ctrl/Cmd+C and
apiRef.copySelectedRows()) included hidden andexportable: falsecolumns in definition order; it now copies the visible columns in screen order. - Copy read
row[field]instead of thevalueGettervalue before formatting. - Values containing tabs, line breaks or quotes broke the pasted table; they are now quoted.
- Only selected rows on the current page and in expanded groups were copied; now every selected row that passes the filter is copied, including pinned rows and collapsed groups.
- Copying right after
selectRowsor fromonRowSelectionModelChangecopied the old selection. - Focus moved to
<body>after copying. - Ctrl+C did not copy with Caps Lock on or with non-Latin keyboard layouts, and it copied while focus was outside the grid or a text selection was active.
- Fields starting with
__(for example__typename) were not copied. - The Ctrl+C listener was registered more than once.
-
slots.toolbarrejectedmemo,forwardRef, class andlazycomponents; switching the toolbar did not remount cleanly; an inline toolbar lost its state. -
slotProps.toolbarrender props had implicitly-anyparameters understrict;slots.paginationtyped withPaginationPropsdid not type-check;groupingColDefrequired afield. - The column menu offered Hide for
hideable: falseand pin actions forpinnable: falsecolumns (including the grouping column). - Manage columns worked only once (after the toolbar button or Escape closed the panel) and did nothing with a custom toolbar; it now opens a standalone panel. The Columns panel closes on Escape and
onColumnsPanelClosefires on every close. - The Summaries panel offered functions outside
availableAggregationFunctionsand omittedunique. - Toolbar panels misaligned with a classic (non-overlay) scrollbar.
- List view passed loading placeholders to
renderCell, paged incorrectly under tree data, ignored server totals, never firedonRowsScrollEnd, and gaverenderCellnovalue,formattedValue,colDeforrowMeta;aria-rowindexrestarted every page. - List view ignored
loading,slots.loadingOverlay,slots.noRowsOverlayandslots.footer, and showed "No Data" while loading;listViewwithoutlistViewColumnrendered nothing (it now falls back to the grid with a dev warning). - List view had no expand chevron or depth indent for tree-data and group parents.
- List view:
renderCellerrors are contained, group rows get no checkbox and fall back to their label, and "N items" counts data rows. - Two column-menu hides or pins in the same tick both apply.
-
DataGridThemeProviderleft part of the palette to the OS colour scheme, sodarkThemein a light browser (or light presets in a dark browser) was unreadable. - Brand presets and
colors.primarydid not recolour the toolbar, filter panel, column menu, selection and focus ring. -
grid.rowHeight*andgrid.headerHeightdid not size rows and the header (compactThemenow gives 36px rows and a 40px header). -
toolbar.*,scrollbar.*,overlays.itemDanger*andgrid.cellFocusBorderhad no effect.
-
The package works on React 18 again. The 2.1.0 build bundled React 19's JSX runtime, which React 18 cannot render (
recentlyCreatedOwnerStackserror on first render). A build check now fails if the JSX runtime is bundled. -
DataGridcrashed under server rendering (renderToString, Next.js, Remix) because a panel readdocumentduring render. -
docs/migration/now ships in the npm package; the v2 → v3 guide was missing fromnode_modules.
- The README,
llms.txtand the AI context file no longer claim the stylesheet loads automatically:import '@opencorestack/opengridx/styles'is required. - The README props tables list only real props, with correct defaults;
llms.txtexamples compile and use real APIs. - Server-side guides describe the real requests; "millions of rows" is replaced by the browser's height limit (about 645k rows at 52px in Chromium).
- The theming docs no longer reference a non-existent
themesexport; the API reference and component pages were corrected against the code.
- Inline
columns(a new array each render with the same definitions) no longer re-runs client filtering and sorting. - The quick filter no longer re-reads every cell on each keystroke.
- Faster date formatting.
-
GridColDef.valueSetter(GridValueSetterParams) maps an edited value back onto the row for editablevalueGettercolumns. -
renderEditCellreceivesonValueChange,onCommitandonCancel(GridRenderEditCellParams); errors in custom editors are contained to their cell. -
formattedValueandrowMetaare passed torenderEditCelland to the function form ofcellClassName, as they already were torenderCell. - Default cell formatting by
GridColDef.typewhen there is novalueFormatter(see Breaking). -
GridFilterOperatorvalues'=','after','onOrAfter','before'and'onOrBefore'; a date input andsingleSelect/ multi-select value controls in the filter panel; the panel notes conditions it cannot show. -
onColumnOrderModelChange(columnOrder)fires with the whole new order after every change, including the Columns panel's Reset. -
disableClipboardCopyprop; Ctrl+C events already handled (defaultPrevented) are left alone. - Export options:
CsvExportOptions.escapeFormulas,CsvExportOptions.bom,ExcelExportOptions.escapeFormulas,PdfExportOptions.font(a Unicode TrueType font), andgetRowIdon every export function. -
GridGroupedExportRow.groupLabel;GridRowMeta.isGroupFooterand theogx__row--group-footerclass for subtotal rows shown in'footer'position. - Keyboard: Shift+Space selects the focused row, Ctrl/Cmd+A selects all, Alt+ArrowRight / Alt+ArrowLeft expand and collapse, Alt+ArrowDown or Ctrl+Enter opens the column menu, Alt+ArrowLeft / Alt+ArrowRight on a focused header resizes it (Shift for 50px steps).
- Enter on a non-editable cell: on a row with children (group rows, tree-data parents) it only toggles expansion, without firing
onRowClickor selecting; on any other row it acts like a click (onRowClick, click-to-select). - Keyboard navigation between rows in list view.
- Column resizing by touch and pen; resize separators expose
aria-valuenow/aria-valuemin/aria-valuemax. -
aria-level/aria-expandedon hierarchy rows andaria-multiselectableon the grid. -
GridTheme.mode('light' | 'dark'),colors.white,colors.black,colors.gray(50–900),grid.cellFontSizeandgrid.headerFontSize. - Type exports:
GridRenderEditCellParams,GridValueSetterParams,GridThemeToolbar,GridThemeOverlays,GridThemeScrollbar,GridThemeSkeleton,GridThemeGrayScale,GridGroupedExportRow,GridRowScrollEndParams,GridColumnOrder,GridDataSourceState,GridDetailPanelHeight,GridAggregationPosition,GridSlots,GridSlotProps, and the props typesCellProps,RowProps,HeaderProps,SkeletonProps,FilterPanelProps,PaginationProps,GridTooltipProps,ButtonProps,InputProps,CheckboxProps. -
Paginationcomponent export. - Optional
rowIdprop onRow, and optionalariaRowIndex/ariaColIndex/columnIndexMapprops on the exportedRow,CellandHeadercomponents. - Optional props on the exported components:
Cell.isPinnedEdge(adds the pinned-section edge class),Cell.valueError(shows avalueGettererror as a cell error),Row.onDetailPanelHeightChange(rowId, height)(measured height of an'auto'detail panel) andRow.isCellEditable. - Dev warnings: the grid stylesheet is not loaded,
pinnedRowsignored under hierarchy, content taller than browsers can scroll, duplicate row ids, empty or duplicate tree paths,valueGetter+editablewithoutvalueSetter, pivot combined withdataSource/ tree data / row grouping,listViewwithoutlistViewColumn. - CSS hooks:
ogx__sticky-top,ogx__sticky-bottom,ogx__header-cell--pinned-left-last,ogx__header-cell--pinned-right-first,ogx__cell--pinned-left-last,ogx__cell--pinned-right-first,ogx__aggregation-spacer,ogx__aggregation-spacer--pinned,ogx__aggregation-footer--loading,ogx__loading-bar,ogx__loading-overlay--over-rows,ogx__loading-overlay--custom,ogx__cell-image,ogx-list-view__expand,ogx-list-view__loading,ogx-filter__hidden-note,ogx-filter__value-multiselect,ogx-col-group-cell--pinned,ogx-col-group-cell--pinned-left/--pinned-right,ogx-column-resize-handle--start,ogx-tooltip--left/--right.
- Sorting is type- and language-aware (
Intl.Collatorwith numeric collation, dates parsed, mixed kinds ordered by kind). - The quick filter searches only visible, filterable columns, using
valueGetterandvalueFormattertext. - Pinned columns render in
pinnedColumns.left/.rightorder; the column menu appends, so the most recently pinned column sits next to the scrolling area. -
onColumnOrderChangeindices are positions in the full current column order (including hidden columns and__group__). - Header drag-reorder works inside a column group when
columnGroupingModelis set (it was disabled for every column); the toolbar and Columns panel can no longer move a column out of its group. - A synchronous
processRowUpdateresult is applied in the same event. -
apiRef.current.copySelectedRows()rejects when the clipboard write fails. -
min/maxof dates return aDate; the defaultavgformat in pivot cells is locale-grouped (56,666.67); the footer exposesaria-busywhile server totals load. -
GridToolbarshows the Summaries button only whenonAggregationModelChangeis passed; toolbar triggers havearia-haspopup="dialog"andaria-expanded. -
GridTooltiprenders into the theme provider (.ogx-theme-provider) withposition: fixedinstead ofdocument.body. -
useGridApiRef()returns aMutableRefObject<GridApi>that is nevernullbefore mount;GridInitialStateaccepts partialcolumns. - The exported
Headerreports a focused header cell asfocusedCell.id === null(was'HEADER'). - The exported
Cellno longer stops propagation of the double-click that starts an edit, so it reaches the row (onRowDoubleClick). -
onStateChangefires on value changes only (it fired for every new prop identity). WithuseGridStateStorage, remount the grid with<DataGrid key={storageKey}>when the key changes; storage is read during the first render, so SSR apps with saved state should render the grid on the client only (see the migration guide, §26). -
GridAggregationPositionis'inline' | 'footer' | null(it was an unused'footer' | 'inline' | 'both'). - Accent colours use CSS
color-mix()(Chrome 111+, Safari 16.2+, Firefox 113+);darkThemegives the toolbar a solid#1e293bbackground.
- Rows can be typed with your own interfaces and type aliases: every public generic is constrained by the new
GridValidRowModel(object) instead ofGridRowModel, and an untypedGridColDef[]can be passed next to typed rows. The README example now compiles understrict. -
DataGridand the export functions,usePivotanduseAggregationhave a typed and an untyped-columns overload. UseDataGridProps<R>rather thanReact.ComponentProps<typeof DataGrid>. -
GridColDefrow callbacks are declared as methods, soGridColDef<Row>is assignable toGridColDefwhenRowis a type alias or extendsGridRowModel. For an interface without an index signature it is not: type the array asGridColDef<Row>[], or leave the columns untyped and use the untyped-columns overload. - Calling
col.renderEditCell(params)yourself with aGridRenderCellParamsis now a type error: its parameter isGridRenderEditCellParams(the old params plusonValueChange,onCommit,onCancel). -
usePivotis declared to returnPivotResult;UsePivotReturnis now a type alias of it (same fields, no declaration merging). - Exported
Header:focusedCell.idisGridRowId | null. ExportedCell:onEditStop(cancel?, field?). ExportedRow:onEditStopparams gain optionalid/field;rowSpanningCaches.hiddenCellOriginMapisRecord<GridRowId, Record<string, GridRowId>>(wasRecord<number, Record<string, number>>). - Slots are typed with the props the grid passes: new
GridToolbarSlotProps,GridPaginationSlotProps,GridOverlaySlotProps,GridFooterSlotProps. KnownslotPropskeys are type-checked. - Export option types are generic (
CsvExportOptions<R>, …), sogetRowIdreceives your row type. - New root exports:
GridValidRowModel,DataGridUntypedColumnsPropsand the slot props types. - Removed unused, never-exported internal types (
GridEditCellProps,GridRowModes,GridRowModesModel,GridDetailPanelContent,GridDetailPanelState,GridVirtualizationState,GridRenderContext,GridAggregationFunction). - The repository is type-checked in CI (
npm run typecheck: library, tests and demo).
-
GridThemeSkeleton.darkBaseColorandGridThemeSkeleton.darkHighlightColor(they had no effect). - The runtime underscore hierarchy fields on rows (see Breaking).
- The OS dark-mode (
prefers-color-scheme) rules for the expand icon and detail panel; the theme palette colours them.
Fixes from a consumer defect report (migration of an ERP report writer from ag-grid) plus issues found while verifying it.
-
Row-grouping subtotals were wrong when data contained nulls —
useRowGroupinghad its own copy of the aggregation functions that coercednullto0(Number(v) || 0). For[10, null, 20]a group showedmin0,avg10 andcount3. It also ignoreduniqueandavailableAggregationFunctions. Group rows now use the same shared functions as the footer (lib/utils/aggregation), giving 10 / 15 / 2. Grouped exports (getGroupedExportRows) inherit the fix. -
min/maxcould throwRangeErroron very large datasets —Math.min(...values)exceeds the engine's argument limit at roughly 120k values. Replaced with loops. -
Expanded groups collapsed on every data change — any new
rowsarray (an inline edit, a live refresh) or a new-but-equalrowGroupingModelarray reset expansion to the default. User choices are now overrides keyed by the grouping config, and are discarded only whenrowGroupingModelordefaultGroupingExpansionDepthchanges value. -
getVisibleRowssorted the memoized tree's child arrays in place under row grouping; it now sorts a copy. -
Tree data reset user expansion whenever rows changed (when
defaultGroupingExpansionDepthwas non-zero). With server-side tree data, a lazily expanded node could collapse as soon as its children arrived. Tree data now uses the same override model as row grouping, and the default expansion applies on the first render instead of after an effect. -
onRowExpansionChange(server-side child fetch) was called inside a React state updater, which React may run twice (always in StrictMode). It is now called exactly once per expand. -
useTreeData().getVisibleRows()crashed whenfilterModelwas omitted, although the param is optional. -
Rows were drawn in the wrong place when rows were pinned to the top.
useGridVisibleRowssubtracted the pinned count from render-window indices that already exclude pinned rows. With N top-pinned rows, every row past the first screen was drawn N rows too low, and the bottom N rows of the viewport were left blank. -
Keyboard navigation and
apiRef.scrollToIndexesleft the target row hidden below the fold. The scroll math ignored the sticky header, so the row ended up about one header height below the visible area. Keyboard navigation also mapped pinned rows onto the wrong layout index. Both now sharescrollRowIntoViewand never scroll to pinned rows, which are always visible. -
valueFormatterwas dropped for every column once row grouping was on — the injected hierarchy renderers fell back to the raw value. They now render the formatted value. (Report D3) -
slots.footer,slots.noRowsOverlayandslots.loadingOverlaywere typed and documented but never rendered. All three are now wired.footerreplaces the pagination area, as documented, and also receivesaggregationResult,rowCount,paginationModel,onPaginationModelChangeandapiRef. (Report D2)
-
onRowDoubleClickonDataGridProps, in grid and list view. (Report D6) -
GridRenderCellParams.formattedValue— thevalueFormatteroutput, now passed torenderCell. -
ColumnVisibilityPanelandColumnVisibilityPanelPropsexported, as the docs already claimed. (Report D5) -
ExcelAdvancedExportOptions.groupedRows— grouped reports inexportToExcelAdvanced, with Excel row outlining, numeric subtotals that keepnumFmt, andgroupHeaderFillColor/groupSubtotalFillColor. (Report D7) -
Development-mode warnings (skipped when
NODE_ENV === 'production'):- when the grid renders every row of a dataset larger than 200 rows because its container has no bounded height (root cause of report D1);
- when
paginationis passed with an activerowGroupingModel(report D4).
-
Real-browser test project — Vitest browser mode with Playwright/Chromium (
npm run test:browser;npm teststays on jsdom).@vitest/browser-playwrightandplaywrightare now declared devDependencies. -
CI:
.github/workflows/npm-publish.ymlruns unit and browser tests before publishing. It previously ran only lint and build, so a failing test could not block a release.
-
features/virtualization.md— new "The grid needs a bounded height" section. It explains the flexmin-height: 0trap and replaces the claim that grouping "works seamlessly" with virtualization. Report D1 turned out to be this: grouped rows are virtualized (a 25,000-row group renders ~20 DOM rows in a bounded container, verified in Chromium), but an unbounded container renders everything, and pagination had been hiding it. Also corrects the claims that columns are not virtualized and that the default overscan is 5. -
features/tree-data-grouping.md,features/sorting-pagination.md— pagination is ignored under row grouping (report D4); expansion and aggregation semantics. -
features/export-guide.md— CSV and basic Excel append two aggregation rows, labels then values (report D8); grouped advanced-Excel export. -
customization/slots-api.md—footerprops.components/column-visibility.md— the examples showed<ColumnVisibilityPanel />with no props, which cannot work; replaced with real usage. -
roadmap.md— removed "upcoming" items that had already shipped (npm publishing, native PDF export, GitHub Pages deployment).
-
Adaptive overscan based on scroll velocity —
useGridScrollSyncnow tracks scroll velocity (px/ms) on every scroll event and maps it to a dynamic overscan tier (3 / 5 / 12 / 20 / 30 rows). All three values (scrollTop,scrollLeft,overscanRows) are bundled into a single state update per RAF frame souseGridVirtualizationrecomputes exactly once per frame. 200 ms after scrolling stops, the overscan decays back to the floor set byoverscanRowCount. -
overscanRowCountprop — newDataGridPropsfield (number, default3). Sets the minimum overscan floor; the adaptive algorithm always produces a value ≥ this prop. Increasing it pre-renders more rows at rest; decreasing it saves idle memory.
Comprehensive accuracy audit — 32 issues corrected across 19 files:
-
Wrong defaults corrected:
pageSizeOptions([10,25,50]→[10,25,50,100]),pinCheckboxColumn/pinExpandColumn(false→true),noRowsLabel('No rows'→'No Data') — inAPI_REFERENCE.md,components/datagrid.md,features/sorting-pagination.md,features/selection.md,components/empty-state.md -
Missing API surface added:
getAllFilteredRows()andgetGroupedExportRows()added toGridApitable;groupLabelfield added toGridRowMeta;groupedRowsoption added toPdfExportOptions;'unique'added to aggregation function type incomponents/aggregation-footer.md -
Non-existent API removed or corrected:
'both'is not a validgetAggregationPositionreturn (valid:'inline' | 'footer' | null);disableColumnResizeDataGrid prop,columnResizeHandle/columnVisibilityPanel/columnGroupHeader/tooltipslots,onColumnWidthChangecallback,disableReorderonGridColDef,showQuickFilterinGridToolbarProps,toolbar/toolbarPropsDataGrid props — all documented as real but never existed; corrected throughout -
Stale content updated:
use-grid-scroll-sync.mdanduse-grid-virtualization.mdparams/returns updated from oldscrollPosRef/scrollTickpattern to currentscrollTop/scrollLeft;grid-row-meta.mdupdated from "will be removed in v2" to "was removed in v2" (correction, 2026-09-16: this specific change was itself inaccurate — the shim was never actually removed in v2.0; reverted to reflect that it is still present and deprecated);roadmap.mdcolumn virtualization claim corrected;virtualization.md"Aui DataGrid" placeholder fixed anddensityprop section added;upgrade-guideversion and slots API corrected
-
Infinite re-render loop with
rowGroupingModel+groupingColDef— when both were active,activeColumnsandeffectivePinnedColumnswere rebuilt as new object/array references on every render. The new references cascaded throughuseRowGrouping's memoized values and auseEffectthat calledsetExpandedGroupIds, triggering a render → new references → effect →setStatecycle that never settled (Maximum update depth exceeded). Both values are now wrapped inuseMemoso their references only change when their actual inputs change.
-
uniqueaggregation function documented in changelog —unique(count of distinct non-null values viaSet) has been a built-in aggregation function since v1. The v2.0.0 changelog entry foravailableAggregationFunctionsnow explicitly lists all six built-in names:sum,avg,count,min,max,unique.
-
Removed dead public API:
onPinnedRowsChangeandonRowGroupingModelChangeare removed fromDataGridProps. Both props accepted callbacks that were never invoked — no pin/unpin-row UI and no drag-to-group UI exist. Callers that passed these callbacks should simply remove them; grid behavior is unchanged.
-
densitywired —density?: 'compact' | 'standard' | 'comfortable'now sets the--ogx-row-heightCSS variable (compact = 32 px, standard =rowHeight, comfortable = 72 px). Previously accepted but silently discarded. -
disableRowSelectionOnClick— clicking a row no longer triggers selection when this prop istrue. Previously wired to internal state only;onRowSelectionModelChangeis now also fired. -
disableMultipleRowSelection— clicking a row while this prop istruecaps selection to that single row (deselects others). Clicking an already-selected row deselects it. -
groupingColDefimplemented — passinggroupingColDefwhenrowGroupingModelis active now creates a dedicated__group__column at position 0, auto-pinned left. Previously the prop was accepted but had no runtime effect. -
groupable: falsehonored — columns withgroupable: falseare now skipped when building the grouping tree inuseRowGrouping. Previously this flag was silently ignored. -
groupingValueFormatteronGridColDef— new optional fieldgroupingValueFormatter?: (params: { field: string; value: unknown }) => stringcustomizes the label shown for group-header rows at that level. The formatted string flows throughGridRowMeta.groupLabel. Falls back to"${field}: ${value}"when omitted. -
availableAggregationFunctionshonored — per-columnavailableAggregationFunctions?: string[]now gates which aggregation functions are computed inuseAggregation. Functions not in the allowed list are skipped for that field. Built-in function names (all available since v1):sum,avg,count,min,max,unique(count of distinct non-null values). -
multiSortprop — newmultiSort?: booleanonDataGrid. Whentrue, every click on a sortable column header appends/cycles that column in the sort model instead of replacing it — no Shift key required. Shift+click continues to work as an append gesture regardless of this prop. -
Multi-sort shift-click — shift-clicking a column header appends the column to
sortModelrather than replacing it. A numbered priority badge appears next to the sort arrow when more than one sort key is active. Plain click still replaces with a single-key sort (unlessmultiSortis set). -
descriptiontooltip —description?: stringonGridColDefnow renders as the nativetitleattribute on the header cell, providing a browser tooltip on hover.
-
Grouped export — all five export functions (
exportToCsv,exportToExcel,exportToJson,printGrid,exportToPdf) now accept an optionalgroupedRows?: GridGroupedExportRow[]option. When provided, the export preserves the row-grouping structure: group-header rows, indented leaf rows, per-group subtotals, and a grand total footer. Flat behavior is unchanged when the option is absent, making this a fully backward-compatible addition. -
GridApi.getGroupedExportRows()— new method that returns a flat ordered list ofGridGroupedExportRowentries (types:group-header | leaf | group-subtotal | grand-total) by traversing the active row-grouping tree. Returnsnullwhen row grouping is not active. -
GridGroupedExportRowtype — new public interface describing each entry in the grouped export list (type,depth,groupField?,groupValue?,aggregatedValues?,row?).
-
GridToolbarColumns panel — columns withhideable: falseare now excluded from the panel list by default. Previously they appeared as permanently disabled rows, adding visual noise for no benefit (users can never toggle them). The column still renders normally in the grid body and in exports — only the Columns panel list changes.
-
GridToolbarProps.showNonHideableColumns?: boolean— opt-out prop. Set totrueto restore the old behavior and showhideable: falsecolumns in the panel as disabled rows. Default:false. No other API surface changed; this is a backward-compatible addition.
-
CellErrorBoundary— movedrenderFn()call into a module-scopeCellRenderTargetchild component so the throw happens in a descendant; a boundary cannot catch errors thrown inside its ownrender(). -
CellErrorBoundary— changedresetKeyfromvaluetorowobject reference so the boundary resets whenever fresh row objects are provided, not just when the cell value changes. Enables "Restore" to clear error state correctly. -
exportToPdf— aggregation footer now computed directly from the exported rows in the demo, bypassing theuseEffect-basedgetAggregationResult()path that could silently returnnulldue to stale closures. -
exportToPdf— aggregation footer text color is now explicitly[0, 0, 0](black) to prevent text from appearing invisible against the light gray footer background. -
exportToPdf— switched to standaloneautoTable(doc, opts)function to avoid unreliable ESM prototype patching in Vite dynamic imports.
-
GridApi.getAllFilteredRows()— returns all filtered and sorted rows regardless of pagination. Use this in export handlers to include every row, not just the current page. -
CellErrorBoundaryDemo— 100 rows, 10 specific rows throw on demand (rows 10, 20 … 100), with correct Restore behaviour.
-
CellErrorBoundarynow accepts arenderFn: () => React.ReactNodeprop and calls it inside its ownrender(), so React correctly catches thrown errors fromrenderCell. Previously the call happened in the parent's render phase — before the boundary — causing the entire app to crash instead of showing a per-cell error indicator. -
exportToPdfdouble-cast throughunknownto satisfy the installedjspdfTypeScript types (the library's class does not exposeautoTable/lastAutoTablein its official type definitions). - Added
jspdfandjspdf-autotabletodevDependenciesso local development and demo builds work without a manualnpm install. - Deploy workflow now copies
index.html→404.htmlso GitHub Pages serves the SPA for direct URL access to any demo route.
-
exportToPdf(rows, columns, options?)— generate a styled PDF report from grid data. Optional peer deps required:npm install jspdf jspdf-autotable. Features: optional branded header (logo, title, filter summary, row count), multi-page data table with repeating column headers, alternating row shading, aggregation footer row. Seedocs/features/pdf-export.md. - New exported type:
PdfExportOptions
-
GridRowMetainterface — hierarchy metadata (hasChildren,treeDepth,isExpanded, etc.) now available viaparams.rowMetainrenderCell. Seedocs/architecture/grid-row-meta.md. -
GridLocaleTextinterface andlocaleTextprop onDataGrid— override all pagination strings for i18n. -
CellErrorBoundary—renderCellerrors are now caught per-cell; the grid continues rendering. Fallback shows⚠in the affected cell. - 25 new unit tests for
useGridRowPipeline,useGridControlledState, anduseGridKeyboardNavigation. -
CLAUDE.mdat project root — AI coding assistant context file. -
docs/architecture/grid-row-meta.md— architecture doc forGridRowMetaandrowMetaMapdata flow.
-
GridRowModelno longer declares_hasChildren,_treeDepth,_isExpanded,_groupingField,_groupingValue,_descendantCountas typed properties. These fields remain on the row object at runtime (backward-compat shim; scheduled for removal in v2.0 at the time of this release — see correction below). Access hierarchy metadata viaparams.rowMetainstead.
-
params.row._hasChildrenetc. — useparams.rowMeta?.hasChildren. Runtime shim scheduled for removal in v2.0 at the time of this release.
Correction (added retroactively): the runtime shim removal described above did not ship in v2.0.0–v2.0.4. The underscore fields are still injected at runtime as of v2.0.4. Removal is deferred to a future major version. See
docs/architecture/grid-row-meta.md.
-
Toolbar render prop slots:
GridToolbarnow accepts render props to replace individual toolbar controls without replacing the entire toolbar. All panels (columns, filters, aggregation) continue to open and close normally — only the trigger element is swapped.-
renderColumnsButton(props)— replace the Columns icon button -
renderFilterButton(props)— replace the Filters icon button; receivesactiveCount -
renderAggregationButton(props)— replace the Summaries icon button; receivesactiveCount -
renderExportButton()— inject an Export button after the Aggregation button (no built-in exists) -
renderQuickFilter(props)— replace the built-inGlobalSearchinput; receivesvalueandonChange
-
-
GridToolbar.className: Accepts an additional CSS class on the toolbar root<div>for full visual override without replacing the component. -
Exported types:
ToolbarButtonRenderPropsandToolbarQuickFilterRenderPropsare now exported from the package for TypeScript consumers. -
Toolbar Customization demo: new demo page at
/toolbar-customizationshowing a branded dark toolbar and a plain light toolbar, each using all five render props.
-
Aggregation footer width misalignment with
flexcolumns: When aGridColDefusedflex(with or without awidthfallback), the aggregation footer cells usedcolumnWidthsto resolve rendered widths. That map is a user-resize override cache — it contains no entry for columns that have not been manually resized, so flex-columns fell back to their rawcol.widthprop value, producing cells that were narrower than the actual column. Fixed by computingresolvedColumnWidthsinDataGridthat seeds from the layout-computed flex widths (unpinnedColsWithWidth,leftPinnedCols,rightPinnedCols) and then overlays any user-resize overrides.GridAggregationFooternow receivesresolvedColumnWidthsinstead ofcolumnWidths.
-
getRowIdnot applied to internal row store:DataGridderivedeffectiveGetRowIdcorrectly but never used it before rows entered the internal state.createInitialStateand theSET_ROWSreducer both indexed byrow.iddirectly, so any consumer passing rows without a nativeidfield would silently collide all rows onundefinedin the lookup map, produceundefinedReact keys, and trigger a "Each child in a list should have a unique key prop" warning. Fixed by normalizingactiveRowsthrougheffectiveGetRowIdintonormalizedRows(viauseMemo) immediately aftereffectiveGetRowIdis derived. The normalization is a no-op when the default(row) => row.idis used, so there is no overhead for the common case.
-
API reference expanded to full surface coverage:
docs/API_REFERENCE.mdgrew from 188 to 1,132 lines. Added 8 new top-level sections — server-side data source (GridDataSource,GridGetRowsParams,GridGetRowsResponse), all event callback param types (GridRowParams,GridCellParams,GridColumnOrderChangeParams,GridRowOrderChangeParams,GridDetailPanelParams), column and row pinning types, full filter model deep reference (per-type operator table,GridFilterGroupnesting), grid state and initial state slice reference, aggregation reference (all 6 built-in functions,getAggregationPositionsemantics), row grouping, column group headers, and list view. DataGrid props section restructured into 13 feature-area sub-tables covering every previously undocumented prop. Developers building server-side sorting + aggregation + pinned columns no longer need to read TypeScript source.
-
GridColDefdocumentation: Added all previously undocumented column properties —flex,minWidth,maxWidth,align,headerAlign,description,editable,renderHeader,renderEditCell,cellClassName,headerClassName,disableColumnMenu,groupable,aggregable,availableAggregationFunctions,valueOptions(forsingleSelect),colSpan,rowSpan. BothREADME.mdanddocs/API_REFERENCE.mdnow carry the complete table. -
Hooks documentation: Expanded the stubs in
docs/API_REFERENCE.mdinto full reference entries.usePivot(was entirely missing),useGridStateStorage(was one sentence — now has a full options + return table and code example),useAggregation(now has params/return tables and built-in function list),useGridApiRef(corrected description and added usage example).
-
Bundled documentation accuracy: All doc files shipped inside the npm package (
docs/) have been corrected to match the actual v1.0.0 API. Key fixes: removed non-existentpageSizestandalone prop (correct API ispaginationModel+pageSizeOptions), fixedheighttype tonumber | string, correctedonColumnOrderChangeparams shape, removed non-existentisRowSelectableandreorderableprops, fixedGridFilterModel.itemstype, added missingGridApimethods (getFilterModel,getAllColumns,setPageSize,getAggregationResult,getAggregationModel,copySelectedRows), and correctedGridInitialStatepersisted state fields. AI agents (Cursor, Copilot, Windsurf) reading bundled docs will now generate accurate code. -
README API reference: Same prop and type corrections applied — complete
apiRefmethod list, new prop tables for Events, Columns, Pinning, Inline Editing, Row Reordering, and updated comparison table.
First stable public release. All 32 demo pages ship with a live source viewer. npm publish workflow is live.
-
npm publish workflow: GitHub Actions workflow (
.github/workflows/npm-publish.yml) triggers on anyv*.*.*tag push — runs lint, build, thennpm publish. The package is now publicly available as@opencorestack/opengridxon the npm registry. -
Source viewer on all 32 demos: Every demo page now uses
DocsLayoutwith a collapsible "View Source" tab showing the full component source code. Previously only 7 of 32 demos had this; the remaining 25 have been migrated.
-
Version:
0.1.xpre-release series →1.0.0stable. The public API (DataGridProps,GridColDef,GridApi, all hooks and types) is now considered stable. -
Demo consistency: All 32 demos share the same
DocsLayoutshell — consistent title, description, live preview, and source viewer. Inline<h1>/<h2>+<p>manual headers removed from every migrated demo.
-
PivotModeDemo—apiRef: any,fallbackRows: any[],props: anytoolbar → fully typed -
InfiniteScrollDemo— introducedPersonRowinterface, allanyin data source and sort params replaced -
ServerSideAggregationDemo—(a as any)[field]field access →keyof Employeekeyed access -
AggregationFooter+ServerSideAggregationDemo—valueFormatter: { value: any }→unknownwith narrowing -
SlotsDemo— addedEmployeeRowinterface,renderCell: (params: any)× 2 replaced -
CRUDTutorial—renderCell: (params: any)→GridRenderCellParams<User> -
RealEstatePortfolio—useState<any>for pinnedColumns →useState<GridColumnPinning> -
ExportDemo—apiRef: any→ReturnType<typeof useGridApiRef>,rowsToPrint: any[]→ typed -
CustomPagination—(props: any)component signature → explicit typed interface -
Editing—handleProcessRowUpdate = (newRow: any)→MockRowinferred from data
-
onRowClicknever fired whenonCellClickwas registered:Cell.tsxwas callinge.stopPropagation()inside its click handler wheneveronCellClickwas provided. This silently ate the event before it could bubble to the row'sonClickhandler, makingonRowClickpermanently unreachable from cell clicks. Removed the stopPropagation — both callbacks now fire in the natural bubble order (cell first, then row), matching standard data grid behavior. -
Column visibility panel list not updating after column reorder: The toolbar was receiving
effectiveColumns(the pre-ordering array, in original definition order) instead oforderedColumns(the reordered array). After a drag-reorder in the panel, the grid columns reordered correctly but the panel list stayed frozen in definition order. FixedtoolbarProps.columnsto useorderedColumns. -
Reset button ignoring column sequence: The Reset button in the column visibility panel called
onShowAllonly (restoring visibility), leaving any user-reordered sequence in place. AddedonColumnOrderResetprop threaded fromColumnVisibilityPanel→ColumnsPanelWrapper→GridToolbar→DataGrid. DataGrid provides() => setInternalColumnOrder(columns.map(c => c.field))to restore original definition order on reset. -
FilterPanelDemofilter panel inaccessible: The demo had noslots={{ toolbar: GridToolbar }}, so the toolbar never rendered and the filter icon never appeared. Added the toolbar slot and rewrote the demo to useDocsLayout(consistent with all other demos). -
EventsDemoevent handlers never fired:onRowClickwas broken by the stopPropagation bug above.onFilterModelChangeandonColumnOrderChangewere dead — no toolbar existed to trigger them. Fixed by addingslots={{ toolbar: GridToolbar }}. Also replaced threeany-typed handler signatures withGridSortItem[],GridFilterModel, andGridColumnOrderChangeParams. -
FilterPaneldebounce stale-closure bug: The 300 ms debounceuseEffectreaditem,col.field, andcurrentOperatordirectly from closure (stale values after operator or field changes), suppressed witheslint-disable-next-line. Replaced with a refs-sync pattern (useLayoutEffectwritingitemRef,colFieldRef,operatorRefeach render) so the effect reads current values without the lint suppression and without stale closures.
-
Type safety — full
anyelimination: Everyanyacrosslib/replaced with explicit types orunknown. Key changes:GridRowModelindex signatureany → unknown(with explicit internal row fields added to the type), all cell/value/error params typed asunknown, aggregation functions typed as(values: unknown[]) => unknown,GridAggregationResulttyped asRecord<string, unknown>, slot component types useRecord<string, unknown>, export utilities narrowed withinstanceof Errorguards. -
No more lint suppressions: Removed all
eslint-disable-next-line react-hooks/exhaustive-depscomments — every case fixed at the root cause rather than suppressed. Methods include: state refactors (scrollTickanti-pattern →scrollTop/scrollLeftstate), ref patterns for stable callbacks, and correct dep arrays. -
DataGrid.tsx continued decomposition: Further hooks and components extracted —
useGridScrollSync(RAF-batched scroll state),useGridVirtualization,useGridVisibleRows,useGridColumns,GridAggregationFooter,GridEmptyState,GridErrorOverlay,GridVirtualRows,GridPinnedRows,GridStandaloneColumnPanel,GridListView. Each module has a single clear responsibility and typed params/return interface.
-
GridApi.scrollToIndexes: New imperative API methodapiRef.current.scrollToIndexes({ rowIndex?, colIndex? })scrolls the viewport to bring any row and/or column into view. Column index addresses all data columns (left-pinned + unpinned + right-pinned); pinned columns are always visible so they are silently skipped. Row scrolling accounts for variable-height rows (expanded detail panels, grouped rows). -
useGridKeyboardNavigationhook: Extracted ~370 lines of keyboard navigation state and handlers fromDataGrid.tsxinto a standaloneuseGridKeyboardNavigationhook. Fixes a previously dead code path where pressing Enter/Space on a header cell never triggered column sort (the edit handler ran first). The hook is part of the publiclib/source. -
useLayouthook: Extracted all layout-computation logic fromDataGrid.tsxinto a standaloneuseLayouthook, reducing the main component by ~350 lines. -
Test suite: Added Vitest +
@testing-library/reactinfrastructure with 65 unit tests coveringfilterRows,sortRows,useAggregation, and a DataGrid smoke test. -
ScrollToIndexes demo: New
/scroll-todemo page showcasing thescrollToIndexesAPI with live row/column index controls.
-
Excel export file format error:
exportToExcelgenerates an HTML-table file withapplication/vnd.ms-excelMIME type (the legacy XLS trick). All demo call-sites were passing explicit.xlsxfilenames, causing Excel 2007+ to reject the download with "file format or file extension is not valid". Changed allexportToExcelusages to.xls.exportToExcelAdvanced(ExcelJS, real OOXML) is unaffected and correctly keeps.xlsx. - Keyboard sort on column headers: Enter/Space on a focused header cell now correctly triggers sort. Previously the generic Enter-edit handler ran first, making header sort unreachable via keyboard.
-
ESLint errors: Resolved all lint errors across the library — hooks called after conditional early returns (
Cell.tsx,Row.tsx), ref mutations in the render phase moved touseLayoutEffect, and portal targets readingref.currentduring render moved touseState + useLayoutEffect.
-
Toolbar Component Identity: Fixed a major bug where defining the
GridToolbarwithin a component's render body produced a new React component reference on every render, causing the toolbar to constantly unmount and remount (destroying all internal states like open panels or typed search text). ReplacedReact.createElementwith direct function invocation in theStableWrapperto bypass React's component-identity check and persist internal DOM state. -
Global Search Focus Preservation: Refactored
GlobalSearchinto an uncontrolled component to prevent continuous data re-renders from stealing focus. Added auseLayoutEffectto automatically restore browser focus to the input field if a React virtual DOM diff incidentally drops it mid-keystroke. - Filter Panel Auto-Dismiss: The Advanced Filter panel no longer collapses indiscriminately when clicking into numeric filter fields or during parent re-renders. Implemented a stable callback ref that prevents the underlying event listeners from rehooking during typing. Click-outside auto-close has been structurally disabled in favor of an explicit "Close" button.
-
Pivot Mode Aggregation: Addressed a critical bug where
aggregationModelwas trying to read base columns (e.g.revenue) on pivot rows that use synthetic column keys (e.g.Q1\u001frevenue\u001fsum), resulting in broken totals.- The aggregation footer now renders synthetic pivot totals correctly.
- The
GridToolbarnow actively provisionseffectiveColumnsto theAggregationPanelto allow users to build summaries on pivot dimensions. - Added a built-in "Grand Total" row appended directly to the
usePivotoutput to generate automatic baseline column totals.
-
Exporting Selected Rows: Corrected data omission in the Demo files where print exports were grabbing the entire dataset instead of respecting active row selection. Used
apiRef.current.getSelectedRows()to extract standard export data without requiring explicit prop-threading.
-
exportableProperty: Addedexportable?: booleantoGridColDef. This allows excluding specific columns (like action buttons, menus, or images) from all export formats (CSV, Excel, JSON, and Print). -
AI-Native Integration: The published npm package now includes raw source code (
lib/) and full documentation (docs/). This allows AI agents (Cursor, Windsurf, Copilot) to "see" the implementation patterns and documentation insidenode_modules, leading to significantly better code generation for downstream users.
- Fixed an issue where the main wrapper
classNamewould erroneously include extra whitespace (e.g.ogx) when no optional classes were active. - Fixed an issue where
onRowOrderChangedrag-and-drop visuals didn't actually update in theEventsDemocomponent examples because it was referencing a static array instead of React State. - Corrected a TypeScript regression where
headerClassNamecomment structure was accidentally broken during the previous update.
- Refined the npm package publication files:
docs/researchanddocs/assets(large binary images) are now excluded to keep the package size lean while retaining all high-value documentation for humans and AI.
- Complete theming support for all advanced dropdown panels (Column Visibiity, Filter Editor, Pivot Mode, Global Search) so they correctly adapt to custom themes via
<DataGridThemeProvider>. - Aggregation, Pivot, filtering, and export capability options now appear directly in the
ThemingDemoexample.
- Replaced the hardcoded portal mounting (
document.body) on popovers to instead intelligently hunt for.ogx-theme-providerto organically inherit user themes in overlay panels. - Fixed GlobalSearch input focus shadow not fully respecting CSS variables.
- Exported missing public types (
GridSortItem,GridApi,GridRowSelectionModel,GridColumnVisibilityModel, etc.) inlib/index.tsto prevent developers from having to derive them manually usingNonNullable.
-
Column Visibility Reorder: Added a drag handle to the
ColumnVisibilityPanelletting users seamlessly reorder columns directly via the Visibility Panel dropdown checkbox list. Uses native HTML Drag and Drop API with no external dependencies. - Added
onColumnReordersupport toColumnVisibilityPanelandGridToolbar.
-
import '@opencorestack/opengridx/styles'now resolves correctly in TypeScript projects. The./stylessubpath export inpackage.jsonnow includes atypespointer todist/opengridx.css.d.ts, eliminating the "Cannot find module" TS error. -
build:libscript now copiesopengridx.css.d.tsintodist/automatically so it's always included in published packages.
-
llms.txtbundled inside the npm package — a machine-readable AI agent API context file with complete props reference, type definitions, and usage examples. Located atnode_modules/@opencorestack/opengridx/llms.txtafter installation.
- CSS now explicitly imported at the barrel entry (
lib/index.ts), ensuring styles are never silently dropped by bundlers (Vite, Webpack, Next.js App Router) that don't auto-resolve side-effect CSS from library packages. - Column resize:
ColumnResizeHandlenow uses the logical stored width (currentWidthprop) instead of reading DOMgetBoundingClientRect(), fixing resize jitter and incorrect delta calculations on second+ drag. - Pinned column resize: Resizing a pinned (sticky) column no longer corrupts its displayed width — the DOM measurement was previously offset by the sticky
left/rightposition, causing an erroneous width jump on first drag.
- Updated
README.mdto accurately describe CSS handling and provide a clear fallback import instruction for all environments.
- Cell editing state now correctly pushes to internal state (
baseRows) instead of being overridden by rigidprops.rowsbindings, preventing data loss on successive edits.
- ExcelJS correctly marked as external in Vite build config (consistent with
peerDependencies) - Clipboard programmatic copy button now correctly reads live selection state via
apiRef.getSelectedRows() -
Ctrl+Ckeyboard shortcut now works when grid checkboxes are focused
- Package size reduced from 8.4 MB → 1.8 MB unpacked (ExcelJS no longer bundled)
- README rewritten with Getting Started first, basic example, and full API reference table
- Cleaned devDependencies (removed unused
strip-comment,strip-comments)
Status: ✅ RELEASE READY — 100% feature-complete for v0.1.0 scope
This is the initial public release of OpenGridX. All planned v0.1.0 features are implemented, tested, and included in the production bundle.
- High-Performance Virtualization — Custom row + column virtual scrolling engine, 60fps at 100k+ rows
- Multi-Column Sorting — Client-side and server-side; stable multi-field sort
- Advanced Filtering — 11+ operators (contains, equals, startsWith, etc.) with AND/OR filter builder UI
- Pagination — Client-side and server-side modes with configurable page sizes
-
Row Selection — Single and multi-row checkbox selection with
rowSelectionModelcontrolled/uncontrolled API
- Column Pinning — Left and right sticky columns with correct z-index layering
- Row Pinning — Top and bottom pinned rows with visual separation
- Column Resizing — Throttled drag-to-resize at 60fps with minimum width enforcement
- Column Reordering — Drag-and-drop column reorder
-
Row Reordering — Drag-and-drop row reorder with
onRowOrderChangecallback -
Detail Panels — Expandable master-detail rows via
getDetailPanelContent -
Cell & Row Spanning —
colSpanandrowSpansupport for merged-cell layouts -
List View Mode — Card-based responsive layout via
listView/listViewColumn -
Column Grouping — Multi-level column header groups via
columnGroupingModel -
Toolbar — Built-in toolbar with column visibility, filter, and density controls; fully replaceable via
slots
-
Inline Cell Editing — Double-click or Enter to edit;
editableper column;processRowUpdatecallback -
Tree Data — Client-side hierarchical rows via
treeData+getTreeDataPath - Row Grouping — Group rows by column value with collapsible groups and aggregation summaries
- Aggregation — SUM, AVG, COUNT, MIN, MAX in group footers and global sticky footer
-
Pivot Mode — Multidimensional data pivoting via
pivotMode+pivotModel
-
Data Source API —
useGridDataSourcehook for unified server-side fetching - Server-Side Sorting, Filtering & Pagination — All offloaded cleanly to the backend
-
Infinite Scroll — Viewport-triggered batch-loading (
paginationMode="infinite") -
Server-Side Tree Data — Lazy children loading via
dataSource.getChildren - Server-Side Aggregation — Fetch summary totals directly from API responses
-
CSV Export —
exportToCsv()utility, respectsvalueFormatter -
Excel Export — Basic
.xlsxviaexportToExcel(); advanced pixel-perfect image-embedded export viaexportToExcelAdvanced()(lazy-loads ExcelJS) -
JSON Export —
exportToJson() -
Print —
printGrid()with print-optimised CSS
-
Keyboard Copy —
Ctrl+C/Cmd+Ccopies selected rows as TSV (tab-separated values) -
Programmatic Copy —
apiRef.current.copySelectedRows()for button-triggered copying - Excel/Sheets Compatible — TSV output pastes cleanly into any spreadsheet app
-
Smart Focus Handling — Does not intercept
Ctrl+Cin text inputs; correctly handles checkbox-focused grid cells
-
DataGridThemeProvider— React context-based global theming -
5 Built-in Themes —
darkTheme,roseTheme,emeraldTheme,amberTheme,compactTheme -
CSS Variables — Full
--ogx-*token system; Shadow DOM compatible -
cellClassName/headerClassName— Per-column custom class injection
- Semantic ARIA roles:
grid,row,gridcell,columnheader -
aria-sort,aria-selected,aria-expanded,aria-readonly,aria-labelthroughout - Full keyboard navigation: Arrow keys, Tab, Enter, Escape, Home/End, PageUp/PageDown
- Visible focus ring in keyboard mode (CSS classname-toggled, zero React state overhead)
-
initialStateprop — Restore column widths, visibility, sort, and filter on mount -
useGridStateStorage(key)hook — Auto-saves tolocalStorage; pluggable storage backend
-
apiRef— Full imperative API:getSelectedRows,copySelectedRows,selectRow,sortColumn,setFilterModel,getVisibleRows,scrollToIndexes, and more -
slotsSystem — Replace Toolbar, Pagination, NoRowsOverlay, LoadingOverlay, Footer -
slotProps— Pass custom props to slot components -
TypeScript — 100% typed; full
index.d.tsoutput viavite-plugin-dts - Zero UI Dependencies — No Ant Design, MUI, or Radix. Pure React + vanilla CSS (BEM)
| Artifact | Minified | Gzipped |
|---|---|---|
opengridx.es.js (ES Module) |
226 KB | 52 KB |
opengridx.umd.js (UMD) |
1,089 KB | 315 KB |
opengridx.css |
59 KB | 10 KB |
exceljs (lazy, Excel export only) |
1,385 KB | 302 KB |
- Rich Excel Styling — Bold headers, background fill, border styles natively via ExcelJS (no post-processing)
- Cell Range Clipboard — Select a rectangular cell region (mouse drag), copy to clipboard, paste from Excel back into editable cells
Last Updated: March 18, 2026
OpenGridX 3.10.1 · MIT · This wiki is generated from docs/ on every push to main. To fix a page, open a PR against the source file.
Start here
Components
- DataGrid
- Header
- Row
- Cell
- Toolbar
- Pagination
- Filter Panel
- Tooltip
- Column Visibility
- Column Grouping
- Column Resizing
- Empty State
- Error Overlay
- Aggregation Footer
Features
- Virtualization
- Filtering & Search
- Header Filters
- Sorting & Pagination
- Custom Pagination
- Editing & Reordering
- Row Selection
- Clipboard
- Cell Range Selection
- Undo & Redo
- Fill Handle
- Pinning
- State Persistence
- Aggregation & Pivot
- Tree Data & Grouping
- Cell Spanning
- Master-Detail
- Keyboard & Accessibility
- Right-to-Left (RTL)
- List View
- Infinite Scroll
- Data Source
- Loading States
- Toolbar Customization
- Export (CSV, Excel, JSON, Print)
- PDF Export
- AI Toolkit (Schema & Validator)
Customization
Upgrading
Contributing
- Contributing
- Testing
- React Compiler
- Roadmap
- DataGrid orchestration
- GridRowMeta
- useGridControlledState
- useGridRowPipeline
- useGridColumns
- useGridVirtualization
- useGridVisibleRows
- useGridScrollSync
- useGridStateSnapshot