Capítulo 29 de 54
cellSpanningFeature merges adjacent body cells (rowspan/colspan style) by recomputing spans from whatever rows are actually rendered — it's fully stateless, so sorting/filtering/pagination/pinning just change adjacency and spans follow automatically, with nothing to persist or reset.
spanRows on the column def: spanRows: true merges adjacent rows with an equal value (compared with Object.is; nullish values never merge, since a merged blank block reads as a bug). Pass a predicate instead — spanRows: ({ anchorValue, value }) => ... — for custom run logic; every candidate is compared against the run's anchor (first) row, keeping runs transitive by construction.spanColumns on the column that should carry the merged content: spanColumns: ({ row }) => row.original.isSummary ? Infinity : 1. The count is measured in actual render order (hidden columns don't count, reordering is handled), values beyond available room clamp to the end of that cell's pinned region, and a span can never cross a start/center/end-pinned boundary.0 on the covered axis — check cell.getRowSpan()/cell.getColSpan() (or the shortcut cell.getIsCovered()) and skip rendering it entirely. Never render rowSpan={0} — in HTML that means "span to the end of the row group," merging the cell down the whole <tbody>, not what you want.0 on at least one axis. Cells only join a vertical run when their column spans also match — a full-width summary row never accidentally merges into the data rows above it.enableCellSpanning: false on the table (global kill switch) or per-column-def.cellSelectionFeature): a selection rectangle auto-expands to fully enclose any merged cell it touches — always entirely selected or entirely unselected, including for exclusions. Arrow-key navigation treats a merge as one stop; getSelectedCellCount() counts a merge once; getSelectedCellRangesData() still returns the full underlying grid since covered cells carry real values even though they don't render.table.getCellSpanIndex() to find the anchor and render a clamped span at the window's top. Grouped columns ignore spanRows (grouping already collapses repeats into group rows). Footer/<tfoot> rendering is unaffected.const features = tableFeatures({ cellSpanningFeature })
columnHelper.accessor('region', { spanRows: true }) // vertical merge
columnHelper.accessor('label', { spanColumns: ({ row }) => row.original.isSummary ? Infinity : 1 }) // horizontal merge
{row.getVisibleCells().map((cell) => {
const rowSpan = cell.getRowSpan(), colSpan = cell.getColSpan()
if (rowSpan === 0 || colSpan === 0) return null // covered — skip, never rowSpan={0}
return <td key={cell.id} rowSpan={rowSpan} colSpan={colSpan}><table.FlexRender cell={cell} /></td>
})}
rowSpan={0} instead of skipping the cell.rowSpan={0} — always skip it (if (rowSpan === 0 || colSpan === 0) return null).table.getCellSpanIndex() to handle a run whose anchor scrolled out of view.header.rowSpan — the header-row equivalent convention this feature mirrors for body cells.