Column Resizing
A column’s default width comes from its content and the table’s layout — usually fine, sometimes not. Long product names need more room than the default; an ID column can shrink. Column resizing gives users a drag handle on the header edge to set the width themselves, and the table remembers their choice once persistence is wired up.
Adding the drag handle
Section titled “Adding the drag handle”Mount ResizeHandle from the shadcn data-table-resize-handle registry item inside a HeaderEdge slot. The handle owns the pointer interaction, the live preview while dragging, and the publish of the final width back to the table — the column declaration only has to expose the slot.
npx shadcn@latest add petyosi/react-virtuoso/data-table-resize-handleThe registry item name is hyphenated; it installs the nested import path @/components/ui/data-table/column-resize.
import { DataTable, DataTableCell, DataTableColumn, DataTableColumnHeader, HeaderEdge } from '@/components/ui/data-table'
import { ResizeHandle } from '@/components/ui/data-table/column-resize'
import { localModel } from '@virtuoso.dev/data-table'
const rows = Array.from({ length: 60 }, (_, index) => ({
name: `Product ${index + 1}`,
category: ['Office', 'Peripherals', 'Audio'][index % 3]!,
stock: 8 + ((index * 7) % 41),
}))
const model = localModel({ data: rows })
export default function App() {
return (
<DataTable className="rounded-xl" model={model} style={{ height: 340 }}>
<DataTableColumn field="name">
<DataTableColumnHeader>
<HeaderEdge component={ResizeHandle} />
{() => 'Product'}
</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="category">
<DataTableColumnHeader>
<HeaderEdge component={ResizeHandle} />
{() => 'Category'}
</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="stock">
<DataTableColumnHeader className="justify-end">
<HeaderEdge component={ResizeHandle} />
{() => 'Stock'}
</DataTableColumnHeader>
<DataTableCell className="text-right tabular-nums">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
</DataTable>
)
}The slot alone is what enables resizing on a column:
<DataTableColumn field="name">
<DataTableColumnHeader>
<HeaderEdge component={ResizeHandle} />
{() => 'Product'}
</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>Skip HeaderEdge for columns the user shouldn’t be allowed to resize — a fixed-width row-selector column, for example.
Where column widths live
Section titled “Where column widths live”The table owns every column’s width through its header. It measures each DataTableColumnHeader, distributes any leftover viewport space across the visible columns, and uses the resulting size for every body cell in that column. The drag handle, the programmatic resizeColumn$ call, and the persisted overrides feed into that same pipeline as user-selected rendered widths. While a resize override is active, sibling columns keep their current rendered widths so dragging a boundary feels direct and can create horizontal overflow.
To set a column’s base width, style the header with width or minimum-width classes. To decide which columns receive leftover horizontal space, use DataTableColumn grow={number}. Do not use Tailwind flex growth classes as the table sizing API:
<DataTableColumn field="id">
<DataTableColumnHeader className="w-20">ID</DataTableColumnHeader>
<DataTableCell>{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>
<DataTableColumn field="name" grow={1}>
<DataTableColumnHeader className="min-w-48">Product</DataTableColumnHeader>
<DataTableCell className="font-medium">{({ cellValue }) => String(cellValue)}</DataTableCell>
</DataTableColumn>For the full sizing model, including fixed metadata columns, text-heavy grow columns, horizontal overflow, and resize override behavior, see Column Layout.
This includes width pressure added for “realistic” content. If a prompt name, badge stack, action menu, or timestamp needs room, express that as a header width:
<DataTableColumn field="updated_at">
<DataTableColumnHeader className="min-w-44">Updated</DataTableColumnHeader>
<DataTableCell className="font-medium">{UpdatedCell}</DataTableCell>
</DataTableColumn>Do not mirror that width on the body cell. For multi-line or nested content, place layout helpers inside the cell renderer:
<DataTableCell>
{({ row }) => (
<div className="min-w-0">
<p className="truncate">{row.data.name}</p>
<p className="truncate text-muted-foreground">{row.data.slug}</p>
</div>
)}
</DataTableCell>As a final review step, search for width utilities on cell declarations:
rg 'DataTableCell.*className=.*(w-|min-w|max-w|basis-|grow|shrink|flex-(none|auto|initial|1|\[))'Resizing without the drag handle
Section titled “Resizing without the drag handle”Some resizes don’t originate from the column edge. A “fit to content” toolbar button, a “Compact / Comfortable / Spacious” preset, a keyboard shortcut — each needs to set the width without anyone dragging. Publish to resizeColumn$ directly:
import { columns$, useEngineRef, useRemoteCellValue, useRemotePublisher } from '@virtuoso.dev/data-table'
import { resizeColumn$ } from '@virtuoso.dev/data-table/column-resize'
const engineRef = useEngineRef()
const columns = useRemoteCellValue(columns$, engineRef)
const resizeColumn = useRemotePublisher(resizeColumn$, engineRef)
function fitNameColumn() {
const nameKey = [...(columns ?? new Map())].find(([, column]) => column.field === 'name')?.[0]
if (nameKey) {
resizeColumn({ key: nameKey, width: 280 })
}
}The payload’s key is the column’s internal identifier, not its field. The lookup against columns$ translates a stable, user-facing field name into the key the stream expects.
Persisting widths across reloads
Section titled “Persisting widths across reloads”The handle records the user’s intent — “the user wants the name column 280px wide.” Intent is what gets saved. Automatic grow output is not persisted; clearing the override lets the column fall back to its measured base width and the current grow distribution.
Mount columnWidthPersistenceAdapter() on DataTableStatePersistence to persist overrides. The adapter saves them keyed by field, so the saved entry survives reordering, partial column lists, and dynamically generated columns. State Persistence covers the full wiring, including the field-name contract that ties saved state to column declarations.
The full resize surface
Section titled “The full resize surface”Three publishers cover the runtime mutations:
resizeColumn$— set a width override for one column.clearColumnWidthOverride$— drop one override; the column falls back to its default.resetColumnWidthOverrides$— drop every override at once, e.g. a “Reset widths” menu item.
One adapter wires those into persistence:
columnWidthPersistenceAdapter()— saves the override map; see State Persistence.
One read-only cell exposes the realized layout, for layout-aware UI that needs the actual rendered widths:
columnWidths$— realized widths after distribution. Read for layout introspection; do not persist these values.
The Column Resizing example puts the handle, preset menus, and persistence together in one table.