Release Notes

DataGrid Changelog

Every release of @sv5ui/datagrid from 1.0.0 on, which is where its API came under semver. It is a separate package with its own version numbers, so nothing here lines up with an sv5ui release of the same name.

v2.1.0 Latest October 11, 2026 GitHub
Breaking Changes
  • DataGridLabels gains 41 required members, the wording a pivot brings. Handing the grid a subset is unaffected, and so is a locale pack of your own; a complete table passed as the base of mergeLabels no longer type-checks, for the same reason it did not at 2.0. Merge onto the built-in table instead docs →
  • sv5ui 2.7.0 is the floor, up from 2.5.0. The header resizes columns through a hook 2.7.0 added, so 2.5 and 2.6 satisfied the old range and gave back a grid whose columns would not resize docs →
  • A feature of your own is now asked for menu items from two places. The column menu calls menuItems with columnId , where only the row context menu called it before, with node . An item returned without looking at which one arrived now appears in every column menu as well docs →
New Features
  • pivot() gathers one field down the page and another across it, with a measure where they meet. The columns come from the data, so a value the data has not held before arrives as a column with no code changed. Every total is measured from the rows rather than added up from the cells beside it, which for an average is the difference between right and wrong. totalColumn , totalRow and columnSubtotal close the crossing docs →
  • A measure can be read as something else. showAs gives a share of its row, its column or the whole, a running total, a change from the crossing before it, or a rank. per divides one measure by another under the same reduction, which is how a margin is read, and valuesOn: 'rows' stacks the measures down the page instead of across it docs →
  • The column axis is yours to shape: columnHeader names a generated header, columnKeys states the keys and their order, orderColumns ranks them by a measure, columnGroups lets a reader fold them, and maxColumns refuses rather than truncates when the data would make too many docs →
  • On the row axis, keepRows keeps the largest or smallest few and can gather the rest into one row so the totals still cover everything, and rowsBehind() answers which rows a cell was measured from docs →
  • Grid.PivotPanel builds a crossing at run time, the column menu offers the two axes, and grid.api gains 28 methods that do the same from code. All of it rides in a saved view docs →
  • A pivot on rowModel: 'server' is answered by the server. The crossing goes out on every request as pivot , the column axis comes back as pivotColumns , and the grid fetches again when the crossing changes. The client never crosses the blocks it happens to hold docs →
  • flashCells() marks a cell with a wash of colour that fades when its value changes, with a direction where both values are numbers. Each mark runs on its own clock, only the cells on screen are remembered, and the look is yours through the flashCell slot, two CSS properties or an appearance option docs →
  • stickyGroups keeps the group rows a reader is inside pinned under the header, one line per level, for grouping() , tree() and pivot() alike. The lines are inert, so they add no tab stop docs →
  • headerHeight and groupHeaderHeight set the leaf header row and each tier of header groups in pixels. Both follow the row height when left off docs →
  • GridFeature.columns lets a feature reshape the columns the grid draws. It is the extension point a pivot is built on, and a column a feature adds is a column like any other docs →
  • A conditional formatting rule can name a pivot measure with column: { measure: 0 } . It paints every crossing of that measure and scales it across the whole crossing, and the XLSX export carries it across docs →
Bug Fixes
  • A saved view could turn grouping on but never off. Nothing was written while nothing was grouped, so a view saved from an ungrouped grid left the grouping it was applied over in place docs →
  • A pinned cell on a grand total row let the rows scrolling behind it show through, its background being 60% opaque docs →
  • The rule above a grand total was drawn twice, and so read heavier than every other one docs →
  • expandAllGroups kept the ids of groups the data no longer holds, so the set of open groups only grew for the life of the grid docs →
  • A row pinned to the top hid behind the header whenever the header was not exactly one row tall docs →
  • showValuesAs divided a whole-column share by a total counted over data rows only, so a column whose numbers live on gathered rows read blank docs →
v2.0.0 September 11, 2026 GitHub
Breaking Changes
  • DataGridLabels gains 147 required members, because the new features brought their own wording and the label table is one table. Handing the grid a subset is unaffected; a complete table of your own passed as the base of mergeLabels no longer compiles, and merges the other way round instead: mergeLabels({ ...myTable, ...overrides }) docs →
  • neq on a number column keeps a blank cell, the way notEqual on a text column always has. A cell with no number in it is not the number being excluded. The two disagreed because one guard was applied to every numeric comparator at once, and the text reading is the one that won docs →
  • A typed column draws text it cannot parse rather than swallowing it. notanumber in a type: 'number' column drew an empty cell while the value sat in the row and the editor opened on it. A blank cell is unchanged and still draws emptyText docs →
  • The loading skeleton's cells carry role="gridcell" . A role="row" owning none is a row a screen reader cannot read. A test asserting no gridcell exists during a fetch should assert no data is on screen instead docs →
New Features
  • grouping() groups by any number of columns and aggregates with thirteen functions, percentile and weightedAvg among them, with group footers, a grand total and a Grid.GroupPanel to drive it. showValuesAs() then reads a number as a share of the grand total, of its parent group or of its row docs →
  • tree() takes either nested children or a parent id and lifts an orphan to the root rather than dropping it, as a real ARIA treegrid . masterDetail() opens a panel of your own under any row docs →
  • formula() compiles an expression to a tree and walks it, with no eval , over 31 functions and a dependency graph that refuses a cycle. The expression stays a string, so it survives a saved view and a link docs →
  • advancedFilter() holds a nested (A AND B) OR C in the operators each column type can answer, and narrows what the column filters already narrowed rather than replacing them. Grid.FilterBuilder draws it docs →
  • rangeSelection() brings spreadsheet ranges: several at once with Ctrl, a fill handle that reads a series out of numbers, dates or text, Ctrl+Enter to write a whole range, cut and move, a clipboard carrying plain text and HTML together, and Grid.RangeStatusBar docs →
  • findReplace() searches the rows a filter left, honours case and whole-cell, and counts what it can write apart from what it found docs →
  • conditionalFormatting() paints colour scales, data bars, duplicates, top N and expression rules. Colours mix through color-mix , so a theme token works, and the rules export into a workbook as Excel's own docs →
  • dataImport() reads CSV, TSV, XLSX and the clipboard, guesses types across locales, remembers a column mapping per grid, validates through each column's schema, and stages the rows in the grid itself so a bad cell is fixed where it sits before anything is committed docs →
  • @sv5ui/datagrid/xlsx writes a real workbook with no dependency: several sheets, styles, Excel's own conditional formatting rules, formula cells, and 200k rows for 26 MB of heap because it writes in chunks docs →
  • savedViews() keeps named views in localStorage or storage of your own and packs a whole grid state into a URL. policy() masks a column, a row or a cell with hide , redact , last4 , email , initials or a function of your own, at render, export, clipboard, search, facet and edit alike docs →
  • serverRowModel() fetches in pages or in blocks as the viewport moves, with placeholders and a fetchAll for exporting the whole set. workerDataSource() loads a columnar copy into a worker, filters and sorts on indices, and falls back to the main thread for a column a worker cannot see docs →
  • commandPalette() gathers what the registered features offer behind Ctrl/Cmd+K , so a feature reaches the palette by being registered rather than by being wired to it docs →
  • GridFeature.component , a component the grid mounts inside its own root, for the work a feature can only do from the render tree: an effect, a listener on the grid element, a layer over the rows. Nothing imports it, so a feature nobody registered is a component nobody bundles docs →
  • GridState.status , which a feature that owns the rows sets so the body can draw a fetch in progress without naming the feature, and getGridContext and getGridElement , which a mounted component had no way to reach from outside the package docs →
  • autoColumns reads a ColumnDef[] off the data, and notEqual joins DateFilterOp in all twelve languages docs →
  • 147 more label keys, 67 to 214, and 66 more slots, 73 to 139, in all twelve languages docs →
Improvements
  • gateReader composes a column's readers once per pass rather than once per cell. 15,000 compositions became three over 5,000 rows and three columns docs →
  • The footer's page range reads 1-25 of 300 in every language. It was an en dash, which is not a character a keyboard has docs →
Bug Fixes
  • The footer no longer runs one page number into the next. Its buttons were square at 2rem and five digits need more, so a grid of twenty thousand pages drew 199971999819999 with the current page clipped inside its own background docs →
  • A boolean column no longer claims a value it does not have. The cell coerced whatever it held, so the strings false and no drew a green tick carrying aria-label="true" . The tick is drawn for a value that reads as a boolean now, and anything else draws its own text docs →
  • A height given to <DataGrid class="h-80" /> holds the rows inside it on a grid that has not registered virtualization() . The class was routed to the root, which has no overflow of its own, while the element that scrolls is the viewport inside it. Measured at h-40 , the rows painted 1,483px below the box and over whatever followed docs →
  • A column that reads its field through an accessor shows what a feature computed for it. grouping() , showValuesAs() and formula() write their result onto the row keyed by column id, and the accessor ran first and unconditionally, so three views of one salary column showed the same sum three times docs →
  • The status bar counts rows rather than the furniture around them. Group headers, group footers and the grand total were counted in the filtered number but not in the total, so sixty rows in five groups reported 65 of 60 rows docs →
  • A snapshot carrying a Date survives a share link. The canonical form rebuilt every object from its entries, which turns a Date into {} , so a link and localStorage disagreed about the same slice docs →
  • A server grid no longer says "no data" while its first request is still out docs →
  • The filter builder reads dates the way the cells do. A condition was written with toISOString , which names the day before anywhere west of Greenwich, and an epoch number was stringified into something no parser reads. It also infers a column's kind from a sample of the grid rather than from the one row it was asked about, walking up to a thousand rows to find fifty values so a column with blank early rows is not called text docs →
  • Replacing without regard to case no longer corrupts the text around a character that changes length when lowercased, and aggregating min or max walks the column instead of spreading it into an argument list, which has an engine limit a grid this size can reach docs →
  • autoColumns no longer reads a column of 1234-56-78 part numbers as dates, and showValuesAs({ percentOfParent }) says so once when the column has no aggregation to divide by instead of drawing a blank column in silence docs →
  • showValuesAs() no longer measures a share against the rows the client happens to hold. On rowModel: 'server' a percentOfGrandTotal divided by the loaded blocks, so one row read 10% with ten rows in and 2.5% with forty. The two whole-column shares are skipped there now, with one warning and a skippedColumns list; percentOfRow still runs because it reads one row docs →
  • Select all on a server grid selects the rows that have arrived and none of the placeholders. With ten of forty rows loaded it selected forty, thirty of them empty rows standing in for a block still on its way docs →
  • A hierarchy the server sends is drawn inside a treegrid . tree() , grouping() and masterDetail() all turn expansion on and serverRowModel({ getRowMeta }) did not, so rows carrying aria-level and aria-expanded sat inside a plain grid , where a screen reader was told five flat rows where there were five groups docs →
  • Applying a saved view, a share link or any snapshot asks a server row model once rather than once per slice it hydrates. A view carrying a sort and a filter cost two requests where a header click costs one, and the first answer was drawn until the second arrived. The refetch coalesces to the end of the tick; an explicit refreshServerRows() still fetches at once docs →
  • advancedFilter() no longer reports itself broken on a server that answers it. serverRowModel() puts the tree on every request as request.advancedFilter , and while a backend was filtering correctly isApplied read false and the console said the feature does not work here. Both account for the server now; without one the warning stands, because then it is the application that has to send the tree docs →
  • A cell that covers the whole row says how many columns it covers. A detail panel, the row that says there is no data and the row that reports a failed fetch all declared aria-colindex="1" and nothing else, which ARIA reads as a span of one. They carry aria-colspan now, counted from the visible columns docs →
  • Find's next match brings the match into view on a grid that scrolls. ensureVisible was registered by virtualization() alone, so on a plain grid the counter advanced, the highlight moved, and a match 682px down a 384px box stayed where it was. The viewport supplies it when no virtualizer does docs →
  • A screen reader is told the same number the status bar shows. The announcement after a filter read the drawn list, so a grouped grid said 9 rows where the bar said 6 of 24 , and it moved when a group was folded. Both read grid.filteredRowCount now, and GridFeature gains a rowCount hook so a feature holding rows off the pipeline answers for its own shape docs →
  • The status bar counts the rows a filter left, not the rows that happen to be open. It read from the drawn list, which does not hold a row inside a collapsed group, so a grouped grid with everything shut reported 0 of 60 rows under five group rows accounting for all sixty. It counts one stage earlier now, through the new grid.filteredNodes docs →
  • The filter builder reads a column of Date objects as dates. The number test ran first and accepts a Date , so the column was offered gt and lt with a field asking for a timestamp instead of before and after with a date picker docs →
v1.3.1 September 3, 2026 GitHub
Bug Fixes
  • A malformed filter snapshot no longer brings the grid down. hydrate cast the slice straight to FilterModel , so an operator belonging to another kind, a set whose values was not a list, or a condition missing the value its operator needs all threw while the pipeline was reading them, and a throw inside a $derived costs the render pass rather than one column. Six of the seven shapes measured against 1.3.0 threw. The slice is sanitized at the boundary now, and a column left with nothing stops filtering, which shows more rows rather than none docs →
  • The filter predicates no longer assume the condition handed to them is well formed. An unknown operator, a missing value, a set whose values are not a list and a kind nothing knows now pass every row instead of throwing. This is the layer under applyFilterModel , which an app can call with a model read back from its own storage docs →
  • A column width the layout cannot draw no longer destroys the grid. A NaN or Infinity width reached the CSS custom property as NaNpx , which is invalid at computed-value time, so the browser dropped the declaration, every column folded into one track and the cells stacked down the page with nothing thrown and nothing logged. Such a width is refused at every route into the property now, and a track that cannot be drawn falls back to the column's minimum docs →
  • Two rows sharing an id no longer fail silently. The row index keeps the last row for a repeated id, so an edit addressed to the row the user opened was written to the other one. A development build names the ids that collided; production pays one integer comparison and nothing more docs →
  • setState no longer throws on a corrupt columns slice. An order that was not an array reached .filter and threw inside the caller's own call. Only string ids order the columns now, only real booleans hide a column or fold a group, and a columns slice that is not an object is taken as nothing at all docs →
  • sorting 's hydrate checked Array.isArray and then cast, so a null entry threw on columnId . Entries that do not name a column and a direction are dropped docs →
v1.3.0 August 24, 2026 GitHub
New Features
  • A filter row under the header: filtering({ floatingRow: true }) , or <DataGrid floatingFilters /> . One field per column in the operator that column already uses, with a date picker, a searchable list of ticks for a set, and a summary that opens the panel for what one field cannot hold docs →
  • Header groups fold. columnGroupShow on a child makes its group foldable, collapsed gives it a starting state, and collapseMode: 'rail' folds the whole group away behind a labelled drawer. The header, the column menu, the keyboard and grid.api.toggleGroup all reach it docs →
  • cellValue : a feature can stand between a cell and every way its value leaves the grid, the cell, the tooltip, CSV, the clipboard, the quick filter's text, a set filter's list and an editor's draft. Sorting and filter predicates deliberately stay raw docs →
  • cellDecoration.style writes declarations onto a cell for the values a class cannot name: a colour scale, a bar width, a custom property a pseudo-element reads docs →
  • headerGroupCell draws a group header the way headerCell draws a leaf one, and the grid keeps its fold control beside whatever the snippet draws docs →
Improvements
  • Date filters are a date picker in the panel as well as in the new row, so a year still being typed is never read as a filter over the whole set docs →
  • The column menu of every column in a foldable group carries the fold action, and the announcer says which way it went docs →
  • A snapshot carries folded groups under columns.collapsed , keyed by group rather than by column, so folding and hiding stay apart docs →
Bug Fixes
  • Focus survives a column disappearing. Hiding a column, or folding one away with its group, used to leave the roving tabindex pointing past the last column, and a grid whose one tab stop that was could not be tabbed into at all docs →
  • A date-only string with a year under 100 is the year it spells. new Date(y, m, d) reads two digits as 19xx, so a field reporting 0002 while someone was still typing jumped to 1902 docs →
v1.2.0 August 18, 2026 GitHub
New Features
  • onExportAll on the toolbar and on Grid.ExportMenu takes over the "all rows" item, for the set the grid does not hold. Under rowModel: 'server' the item is named "Loaded rows" without it, because one page is all there is to write docs →
  • The server request carries two decisions it used to leave to guesswork: quickFields , the columns a bare query applies to, and nulls on every sort entry, written as the side blanks actually land on so ORDER BY ... NULLS LAST reproduces the client ordering docs →
  • initialColumns on column virtualization bounds what renders before the viewport is measured, which is every server-rendered paint. Twenty thousand columns used to mount every one of them for a frame docs →
  • A locale pack may be partial. English answers for whatever it leaves out, so a pack of five strings works and one written against an older version keeps building when a new string is named docs →
Improvements
  • The quick filter matches the text a cell draws as well as the value behind it, so a currency column answers to $127,691.00 and a percent column to 45% . The text is built once per row and held, so the first keystroke pays for the pass and the rest are a substring test each docs →
  • Sorting reads each column once per row rather than twice per comparison, sorts positions rather than nodes, and specializes the comparator to a column of plain numbers or strings. An accessor used to be called on the order of 1.7 million times for 100k rows docs →
  • A number, currency or percent column parses what an editor or the clipboard hands it, so the row keeps holding a number rather than the string that was typed. Text that is not a number goes to validation rather than being stored as NaN docs →
  • The column range is found by binary search rather than by walking from the first column, so scrolling far to the right no longer costs the whole column list per frame docs →
Bug Fixes
  • Dates stop drifting by a day. A date-only string is the calendar day it spells rather than UTC midnight, date filters compare calendar days in the reader's own zone and accept epoch numbers, a date column sorts as dates whatever form a row holds, the date editor opens on a Date object, and CSV and clipboard write the day the cell drew rather than a UTC instant docs →
  • A set filter keys its values the way its checklist does, so a column holding Date objects matches what was ticked and a filter restored from a snapshot is the one that was saved docs →
  • A percent column's filter panel speaks percentages: type 5 to find the cells reading 5%, while the model keeps the row's own units, so a persisted filter and a server request still mean 0.05 docs →
  • Enter commits without turning the page. Committing on the last row of a page used to page the grid, taking the row just edited off the screen; arrow keys still cross docs →
  • A Select or a date editor inside a row edit draws one focus ring rather than two, through the new cellEditorInRowWidget slot docs →
v1.1.0 August 13, 2026 GitHub
New Features
  • A cell snippet receives formatted : the text the built-in renderer would have printed. Declaring type and cell together is how a column keeps its formatting and decorates around it, rather than restating its own typeOptions docs →
  • Every cell callback receives column - cell , cellClass , tooltip , colSpan and rowSpan . A renderer that needs its own def, alignment or id had no way to reach them docs →
  • formatted on exportCsv and copySelection writes what the grid is showing rather than the value behind it. Off by default, because a spreadsheet wants a number it can sum docs →
Improvements
  • tooltip: true shows what the cell shows, through sv5ui's Tooltip . It used to return the raw value as a native title , so a currency column reading $204,000.00 had a tooltip saying 204000 . The automatic tooltip for clipped text is untouched docs →
  • Twenty-eight symbols left the package entry, the formatters among them. A 1.0 surface should carry what an app does with the grid rather than what happens to exist inside it; the formatted field above is what replaced the formatters docs →
Bug Fixes
  • toSortRequest finds a column that lives under a header group. The lookup was a flat find, so a grouped grid sent an empty sort: the arrow moved, the request went out, and the rows came back in the order they left, with nothing to say why docs →
  • ui.headerCell typography reaches a sortable column's label. The label sits in the button that toggles the sort, and a button refuses text-transform and text-align from its parent, so a themed header came out in the wrong case docs →
  • A row pinned to the bottom draws its separator on the edge facing the rows rather than the one facing the grid border, where it read as a doubled line docs →
v1.0.0 August 10, 2026 GitHub
New Features
  • rowCountChanged - emitted by setRowCount under rowModel: "server" when the total differs from the one before. It is the moment a server model learns how many rows it has, and the announcer reads that count rather than counting the page docs →
  • registerDataGridIcons and datagridIcons are exported. Nothing needs to call the registrar now that the import covers it, except a grid behind a dynamic import() whose module may load after the app draws its own icons docs →
Improvements
  • Select-all adds the rows in view to the selection instead of replacing it, and clearing takes those rows back out. Under a server model, ticking the header on page 2 used to drop everything chosen on page 1; under a filter it dropped the rows the filter hid. clear() still empties the selection docs →
Bug Fixes
  • setQuickFilter from app code survives the toolbar. The quick filter box owned both directions of the sync in one effect and wrote its own value back over the call in the same flush, so a saved view, a deep link or a "clear all" button was a silent no-op wherever <DataGrid toolbar /> was used docs →
  • A grid on rowModel: "server" stays on the page it is showing. Focusing a body cell turned to the page that row sits on, arithmetic that only holds when the grid holds the whole dataset, so every click past page 1 snapped back to page 1 and fetched it docs →
  • A server model numbers its rows from the page it holds. The row index was offset by the page, so Space , Ctrl+C and type-to-edit reached the wrong row or nothing at all, and aria-rowindex ran past the aria-rowcount the grid reported docs →
  • A server model tells a screen reader where in the whole set the page sits. aria-rowcount counted one page and aria-rowindex counted from 1 within it, so every page read as "row 1 of 10" docs →
  • A click anywhere in the selection column toggles its row. The checkbox is 18x18 inside a 44x40 cell, so 18% of the column was live and the rest looked identical and did nothing docs →
  • The bundled icons register when the grid is imported rather than when a grid mounts, so an icon the app drew first no longer flickers in when its fetch lands docs →