Column Resize Table
Drag column edges to resize widths, or double-click a grip to autosize to content.
Drag any header’s right edge to resize that column, or double-click the grip to autosize to the widest visible content. Drop <DataTableColumnResize /> inside DataTableRoot to enable it: columns keep working with regular DataTableHeader / DataTableBody (no virtualization required). Opt out per column with enableResizing: false.
Columns default to a 40px drag floor (minSize) when resize is enabled: override per column with minSize / maxSize. Body cells use truncate so long text ellipsizes instead of spilling into the next column (same as the Data Grid / virtualized bodies).
Fill is on by default. With resize enabled the table stretches to its container and one flex column (the first non-pinned data column) absorbs the leftover width, so a trailing actions column pins to the right edge instead of leaving dead space. Un-resized columns are also floored at their header width so labels never truncate on load. See Fill and header-fit for pointing the fill at another column, opting columns out, or turning filling off for the table.
Prefer to spread the surplus across every resizable column instead of one flex column? Add <DataTableColumnAutoFit /> to scale them equally on load, backing off once you drag a column or restore a saved layout. See Auto-fit and Persist Widths.
Preview with Controlled State
Introduction
Section titled “Introduction”Column resizing lets users tweak widths for dense, multi-column tables: especially when content length varies (emails, notes, product names). Explicit size values make the initial layout predictable; the resize addon handles drag and double-click autosize.
Installation
Section titled “Installation”Install the DataTable core and add-ons for this example:
First time using
@niko-table? See the Installation Guide to set up the registry.
For other add-ons or manual copy-paste, see the Installation Guide.
Prerequisites
Section titled “Prerequisites”We’ll build a table showing orders. Here’s our data:
type Order = { id: string customer: string email: string product: string amount: number status: "pending" | "shipped" | "delivered" | "cancelled" date: string region: string city: string payment: string shipping: string notes: string}
const data: Order[] = [ { id: "ORD-001", customer: "John Doe", product: "Premium Widget", amount: 299.99, status: "delivered", date: "2024-01-15", region: "North America", city: "New York", payment: "Credit Card", shipping: "Express", notes: "Leave at front desk", }, // ...]Enable Column Resize
Section titled “Enable Column Resize”Drop <DataTableColumnResize /> early inside DataTableRoot. Its presence flips enableColumnResizing via feature detection: no config flag required.
<DataTableRoot data={data} columns={columns}> <DataTableColumnResize /> <DataTable> <DataTableHeader /> <DataTableBody /> </DataTable></DataTableRoot>While you drag, a guide line follows the cursor and the column applies its new width on release (columnResizeMode: "onEnd"). Widths aren’t rewritten on every mousemove, which keeps large and virtualized tables smooth during a drag.
Keyboard
Section titled “Keyboard”The grip is keyboard-operable with WAI-ARIA separator semantics: Tab to it, then resize without a mouse.
| Key | Action |
|---|---|
Arrow Left / Right |
Nudge the width by 8px |
Shift + Arrow |
Nudge by 40px |
Enter |
Autosize to fit content |
Column Definitions
Section titled “Column Definitions”Set explicit size on columns so the initial layout is meaningful and horizontal scroll appears when needed. Opt out of resizing on fixed columns (e.g. actions):
const columns: DataTableColumnDef<Order>[] = [ { accessorKey: "id", size: 110, header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Order ID" /> <DataTableColumnActions> <DataTableColumnSortOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Order ID" }, }, { accessorKey: "customer", size: 160, header: () => ( <DataTableColumnHeader> <DataTableColumnTitle title="Customer" /> <DataTableColumnActions> <DataTableColumnSortOptions /> </DataTableColumnActions> </DataTableColumnHeader> ), meta: { label: "Customer" }, }, // ... more columns with explicit sizes { id: "actions", size: 70, enableResizing: false, // no resize grip on this column enableSorting: false, enableHiding: false, // ... },]Fill and header-fit
Section titled “Fill and header-fit”Fill and header-fit come with <DataTableColumnResize />: no extra install, no config. One flex column absorbs the leftover width (pure layout, never written to columnSizing), and it stays resizable — dragging its grip pins it at an explicit width and hands the fill to the next eligible column. By default the fill goes to the first non-pinned data column; steer it per column with meta:
// Point the fill at a specific column (default: first non-pinned data column){ accessorKey: "description", meta: { flex: true },}
// Opt a column out of being auto-picked{ accessorKey: "notes", meta: { flex: false },}To turn filling off for the whole table (e.g. a wide table meant to scroll horizontally), pass disableFlexFill through the table-level meta option on DataTableRoot — it’s a table option, not column meta:
<DataTableRoot data={data} columns={columns} meta={{ disableFlexFill: true }}>disableFlexFill is read when the table options are built, so set it statically rather than toggling it at runtime.
Header-fit keeps labels legible: un-resized columns are floored at their header label width so labels don’t truncate on load, while a column the user has resized always keeps exactly its width. If a column’s visible title comes from <DataTableColumnTitle title="..." /> rather than meta.label, mirror it into meta.label so header-fit measures the right string.
Auto-fit
Section titled “Auto-fit”By default a single flex column absorbs the leftover width. When you’d rather distribute the surplus evenly across every resizable column, drop <DataTableColumnAutoFit /> next to <DataTableColumnResize /> and the resizable columns scale up to fill the container on load. It’s a self-contained, opt-in feature: without the marker, the default single-flex-column fill applies (the first non-pinned data column absorbs the surplus, un-resized columns floored at their header width) and none of the auto-fit code ships. Fixed columns (enableResizing: false) keep their size; auto-fit re-fits when the container grows and backs off once the user drags a column or a saved columnSizing is restored, so manual and persisted widths win. Columns wider than the container scroll horizontally.
Install it alongside resize:
<DataTableRoot data={data} columns={columns}> <DataTableColumnResize /> <DataTableColumnAutoFit /> <DataTable> <DataTableHeader /> <DataTableBody /> </DataTable></DataTableRoot>These columns declare small size values yet fill the width:
Controlled Sizing
Section titled “Controlled Sizing”Manage columnSizing externally for persistence or live inspection. Provide state={{ columnSizing }} + onColumnSizingChange. Restored widths take precedence over automatic sizing (both the default fill and auto-fit), so a saved layout is never overwritten on reload:
import { useState } from "react"import { type ColumnSizingState } from "@tanstack/react-table"
export function ControlledResizeTable({ data }: { data: Order[] }) { const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({})
return ( <DataTableRoot data={data} columns={columns} state={{ columnSizing }} onColumnSizingChange={setColumnSizing} > <DataTableColumnResize /> {/* ... */} </DataTableRoot> )}Persist Widths
Section titled “Persist Widths”Want resized widths to survive a reload? Install the opt-in useColumnSizingPersistence hook and wire its columnSizing + onColumnSizingChange into DataTableRoot. It’s localStorage-backed, writes immediately, syncs across tabs, and ignores corrupt storage. Resize a column below, then reload:
import { useColumnSizingPersistence } from "@/components/niko-table/lib/use-column-sizing-persistence"
export function OrdersTable({ rows, orgId }) { // Scope the key per table (and per tenant/user if widths shouldn't be shared). const { columnSizing, onColumnSizingChange } = useColumnSizingPersistence( `orders-table:${orgId}`, )
return ( <DataTableRoot data={rows} columns={columns} state={{ columnSizing }} onColumnSizingChange={onColumnSizingChange} > <DataTableColumnResize /> <DataTable> <DataTableHeader /> <DataTableBody /> </DataTable> </DataTableRoot> )}The hook also returns resetColumnSizing() to clear the saved widths. Because widths are a per-user preference, they sync across tabs: resizing (or resetting) the same table in one tab updates the others via the native storage event. Idle tabs only re-render when a width actually changed.
Prefer to own the storage yourself (e.g. sync widths through a per-user settings API instead of localStorage)? It’s a thin wrapper — copy it and swap the store; all DataTableRoot needs is a controlled columnSizing + onColumnSizingChange.
When persistence and <DataTableColumnAutoFit /> are combined, the fitted widths are saved and restored like any user resize — so auto-fit runs once per user, not on every visit (restored widths always win).
Persist column width like visibility and order: a per-user preference, not URL state. Restored widths take precedence over automatic sizing, so a saved layout is never overwritten.
When to Use
Section titled “When to Use”✅ Use Column Resize when:
- Tables have many columns and users need to adjust widths to content
- You want drag-to-resize and double-click autosize without a custom header
- Some columns (row number, actions) should stay fixed-width via
enableResizing: false
Works with:
- Row DnD: same
<DataTableColumnResize />onDataTableHeader+DataTableDndBody(opt out on drag-handle columns)
❌ Consider other options when:
- All columns fit comfortably and widths never need tuning
- You only need sticky identifier/action columns: see Column Pinning
Virtualization
Section titled “Virtualization”You do not need a separate “virtualized column resize” example. Swap in DataTableVirtualizedHeader + DataTableVirtualizedBody (and give DataTable a fixed height): the same <DataTableColumnResize /> marker applies. See Virtualization Table and Composed Grid.
Next Steps
Section titled “Next Steps”- Column Pinning Table - Keep key columns visible while scrolling
- Virtualization Table - Same resize marker with virtualized headers/bodies
- Composed Grid - Resize + editable grid cells