Smart Column Width
Size columns to their content. Opt-in via the React <Table autoColumnWidth>
prop — off by default, so existing layouts are unchanged.
How to Enable
// Default policy: fit to container, leave underflow, sample 100 rows.
<Table table={table} autoColumnWidth />
// Tuned:
<Table
table={table}
autoColumnWidth={{
sampleSize: 100, // rows sampled for measurement (default 100)
overflow: 'fit', // 'fit' | 'scroll' (default 'fit')
underflow: 'leave', // 'leave' | 'distribute' | 'stretch' (default 'leave')
fitThreshold: 0.15, // compress instead of scroll when overflow ≤ 15% (default 0 = off)
}}
/>
Overflow modes (natural total > container)
fit— never scroll horizontally. Over-wide columns are squished and their cells wrap, taking width first from the columns with the most slack, never below a floor (minSize, or 48px).scroll— keep natural widths and allow horizontal scrolling.
Small-overflow compression (fitThreshold)
A few-pixel horizontal scrollbar is almost always an accident. fitThreshold (a
fraction of the container, e.g. 0.15 = 15%) says "keep it in view if it nearly
fits": when natural widths overflow by ≤ the threshold, columns are
proportionally compressed to fit without wrapping (respecting minSize)
instead of scrolling. Above the threshold, the normal overflow behavior applies.
Because compression is pure width math (no wrapped-row heights to measure), it
works even under row virtualization — unlike overflow: 'fit', which wraps and
so falls back to scroll when virtualized. It also survives async re-measures and
never touches user-resized columns.
Underflow modes (natural total < container)
leave— columns keep their natural width.distribute— extra space is spread proportionally across auto-sized columns in a waterfall that respects eachmaxSizeas a hard cap: a column that would exceedmaxSizeis pinned and its leftover cascades to the uncapped columns. A gutter remains only when every auto column is capped.stretch— same waterfall, butmaxSizeis a soft cap: if space is still left after all columns cap, they grow pastmaxSizeproportionally so the container fills exactly — never a dead gutter (handy for modal/settings tables with a few capped columns).
Per-column opt-out
A column with an explicit size or enableAutoSize: false keeps its width and
is excluded from measurement and squishing:
columnHelper.accessor('status', { header: 'Status', size: 120 })
columnHelper.accessor('actions', { header: '', enableAutoSize: false })
Computed widths flow through columnSizing, so pinned offsets, the <colgroup>,
and virtualization totals stay in sync. Dragging a column's resize handle
excludes it from further auto-sizing.
Measuring rendered content
By default a column is measured by its raw accessor value. When the cell renders
something wider — a formatter or custom cell (accessor 19 shown as
"$19.00 (Gold)") — the raw value under-measures and clips. Two per-column
overrides fix it:
columnHelper.accessor('price', {
cell: ({ getValue }) => formatCurrency(getValue()),
autoSizeText: (row) => formatCurrency(row.original.price), // measure what renders
})
columnHelper.accessor('trend', {
cell: ({ getValue }) => <Sparkline value={getValue()} width={80} />,
autoSizeWidth: () => 80, // exact px, bypasses text measurement (no padding added)
})
Precedence: autoSizeWidth (exact px, verbatim) → autoSizeText (measured
string, gets padding + sort-indicator) → raw accessor value.
Async values (re-measure)
When cell values arrive after mount (rows render with placeholders like $0
/ '-', then real values merge in from a separate query), the first measurement
sizes on the placeholder. Auto-sizing corrects this by re-measuring whenever the
row data reference changes identity — an async merge produces a new data
array, which triggers a debounced (~60ms) re-measure. Sorting, filtering, and
pagination do not re-measure (they change the derived row model, not data).
Re-measure respects provenance: only auto-owned widths update; user-resized and
persisted widths are untouched.
If values merge without a new data array (in-place mutation), call the
escape hatch when the merge is done:
table.remeasureColumns() // immediate re-measure (no debounce)
table.remeasureColumns('prices-loaded') // optional reason, forwarded on the event
table.remeasureColumns(reason?) emits 'columns:remeasure'; it is a no-op when
smart width is off and only re-sizes auto-owned columns.
minSize is a hard floor
A column can never render below its minSize — core getSize clamps every width
up to minSize. Auto-sizing treats it as a hard floor: content narrower than
minSize renders at minSize. Want tighter auto-sizing? Lower or omit minSize.
Persisted widths
The hook never overwrites a width it didn't write this session — a user-dragged
width or one restored from persisted columnSizing is treated as user-set and
left alone.
Persisting auto-sized widths neuters smart width. If you save the whole
columnSizingstate (it includes the widths auto-sizing computed) and restore it on the next load, those restored widths look user-set to the provenance rule and are skipped — the column stops auto-measuring, and async re-measure andstretchwon't touch it. Prefer not persistingcolumnSizingunderautoColumnWidth(auto recomputes from content every load), or persist only the columns the user actually resized (mark them at resize time) and restore just those.
Limitations
- Custom cell renderers (badges, progress, custom components) or formatted
text measure by the raw string value — use
autoSizeText/autoSizeWidthabove, or give them an explicitsize/enableAutoSize: false. - Row virtualization: wrapped row heights aren't measured, so under row
virtualization
overflow: 'fit'falls back toscrollbehavior and logs a one-time dev warning. The supported way to get fit + wrap under virtualization is Pretext-measured row heights; otherwise disable virtualization or setoverflow: 'scroll'.