pageSize: 0 disables pagination — all rows are returned on a single page. All four adapters default to 0. processedData holds all filtered/sorted rows regardless of pagination (used for toggleSelectAll, total count, etc.).
computeTotalPages/paginateData (core) treat a non-finite pageSize (e.g. NaN, from an unvalidated input forwarded straight to setPageSize) the same as pageSize <= 0 — pagination disabled, all rows returned — rather than propagating NaN into Math.ceil/Array.prototype.slice math, which would otherwise silently collapse the table to an empty page (slice(NaN, NaN) coerces both bounds to 0). paginateData similarly falls back page itself to 1 when it's non-finite. React/Vue/Solid's setPage/setPageSize actions guard at the call boundary too — a non-finite argument is a no-op (state is left untouched) rather than being stored and then relied on to be caught downstream.
Grouping and pagination compose as: group the full processedData → flatten to visible items (getVisibleRows, respecting collapse state) → paginate that flattened header+row sequence → re-chunk the page's slice back into per-group form for rendering. This is a deliberate reversal of the more obvious "paginate data rows, then group whatever lands on that page" order — that order would let a page silently render pageSize data rows plus however many header rows happened to land on top of them, so a pageSize: 20 table with grouping active could visibly render 23+ rows. Counting header rows against the same budget as data rows means a page never renders more than pageSize rows total, at the cost of a group's rows now being able to split across a page boundary purely because the budget ran out mid-group (this already could happen in a milder form before — grouping was never sort-stable across the old pagination boundary either, so the same group value could already appear as two separate header occurrences on two different pages).
paginateVisibleGroups(groupedFull, visibleItems, collapsedGroups, defaultCollapsed, page, pageSize)(core) is the re-chunking step, returningPagedGroup<TRow>[]— aGroupResultpluscontinued: booleanandsampleRow: TRow | undefined. It's the inverse ofgetVisibleRows, scoped to one page:paginateData(generic enough to reuse as-is forVisibleItem<TRow>[]) slicesvisibleItemsfor the page, then a single pass rebuilds group chunks from that slice.continuedistruewhen a chunk's header is a repeat — the group's real header rendered on an earlier page and this page's slice starts mid-group (a'row'item whosegroupKeydoesn't match the currently-open chunk). Each adapter renders acontinuedchunk's header identically to a real one but appendsL.groupContinued(e.g. "(cont'd)") next to the row count, so a returning header reads as a continuation rather than an unrelated duplicate group.- A collapsed group's
rowsare backfilled from the full group (groupedFull) rather than accumulated from the page slice, because a collapsed group contributes exactly one item tovisibleItems(its header, no rows) — it never enters the paginated flow at all, so there's no "this page's portion" of it to fall back to. This also happens to be the more useful answer: collapsing a group is usually precisely so its aggregate/select-all reflect the whole group, not whatever fraction of it shares the current page. An expanded group'srows, by contrast, really is just the chunk visible on this page — consistent with the pre-existing behavior that a group's aggregate was never a true full-dataset total, only ever scoped to whatever page/slice it appeared in. sampleRowis a representative row for the group (its first row ingroupedFull, or the row itself for an ungrouped chunk), always present whenkeyis non-null — independent of whetherrowshappens to be empty for this specific page occurrence. This matters because a header can legitimately be the very last item of a page's budget with zero of its own rows following until the next page (an expanded group whose first row is pushed to the next page); rendering the header's groupBy value (via the column's real accessor/formatter) and the aggregate row'scol.format(v, row)both need some row to read from, androws[0]would beundefinedin exactly this case. Every adapter readssampleRow(neverrows[0]) for these two things, whilerowsitself remains correct for the checkbox/aggregate-computation purposes above.
paginateVisibleItems(visibleItems, page, pageSize)(core) is the equivalent per-page slice for keyboard navigation (see Keyboard navigation): a plainpaginateDataslice, except when that slice starts with row items whose group header rendered on an earlier page — then a synthetic{ kind: 'group', key }item is prepended so the repeated ("continued") header, a real focusable row once rendered, is a valid Tab stop here too.pagedData(still exposed fromuseTableState/createTableState, unchanged typeTRow[]) is now derived from the same paginated flattening —paginateData(visibleItems, page, pageSize)filtered to'row'items and mapped to.row— rather than a flat data-row slice, so it reflects the data rows actually visible on the page (fewer thanpageSizewhenever headers share the budget), not a fixed-size chunk ofprocessedData.numPagesiscomputeTotalPages(visibleItems.length, pageSize)—visibleItems.length(headers + visible rows) rather thanprocessedData.length, so it grows when groups are expanded and shrinks when they're collapsed. Toggling a group's collapse state can therefore changenumPagesout from under the currentpage; this is handled the same way a filter change already could shrinknumPages— no state reset on toggle, just the pre-existingMath.min(page, numPages)clamp applied whereverpageis used.- The toolbar's group count stat dedupes
groupedDatabykeybefore counting (new Set(groupedData.map(g => g.key)).size) rather than usinggroupedData.lengthdirectly, since a split group'scontinuedchunk would otherwise be counted as a second group.
The «, ‹, › and » buttons are named by the firstPage/previousPage/nextPage/lastPage labels (aria-label), since the glyph alone reads as nothing useful.
The "Rows per page" <select>'s options are hardcoded to [10, 20, 50, 100] in each adapter, but a consumer's initialViewState.pageSize/setPageSize isn't restricted to those four — a plain <select> bound to a value absent from its own <option>s can't select anything, so the browser falls back to displaying the first option, silently showing the wrong number while the actual pageSize (and the row count on screen) stays correct. mergePageSizeOptions(options, pageSize) (core) inserts pageSize into the option list (sorted) when it's missing, so the dropdown always has a matching option; each adapter calls it with its own [10, 20, 50, 100] base list rather than rendering it directly.
Part of the @vates/data-table architecture docs — see CLAUDE.md for the project overview.