Skip to content

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.

Open in
Order ID
Customer
Email
Product
Amount
Status
Date
Region
City
Payment
Shipping
Notes
Actions
ORD-001John Doe[email protected]Premium Widget$299.99delivered1/15/2024North AmericaNew YorkCredit CardExpressLeave at front desk
ORD-002Jane Smith[email protected]Basic Kit$149.50shipped1/18/2024EuropeLondonPayPalStandardGift wrap requested
ORD-003Bob Johnson[email protected]Pro Bundle$599.00pending1/20/2024Asia PacificTokyoBank TransferEconomyCall before delivery
ORD-004Alice Williams[email protected]Starter Pack$79.99delivered1/22/2024North AmericaChicagoCredit CardStandardNo special instructions
ORD-005Charlie Brown[email protected]Enterprise Suite$1,299.00shipped1/25/2024EuropeBerlinInvoiceExpressBusiness address only
ORD-006Diana Prince[email protected]Premium Widget$299.99cancelled1/28/2024Asia PacificSydneyCredit CardStandardCustomer cancelled
ORD-007Ethan Hunt[email protected]Basic Kit$149.50pending2/1/2024North AmericaLos AngelesPayPalExpressSignature required
ORD-008Fiona Green[email protected]Pro Bundle$599.00delivered2/5/2024EuropeParisCredit CardStandardApartment 4B
ORD-009George Miller[email protected]Starter Pack$79.99shipped2/8/2024Asia PacificSingaporeBank TransferEconomyOffice hours only
ORD-010Hannah Lee[email protected]Enterprise Suite$1,299.00delivered2/12/2024North AmericaSeattleInvoiceExpressInclude packing slip
Preview with Controlled State
Open in
Order ID
Customer
Email
Product
Amount
Status
Date
Region
City
Payment
Shipping
Notes
Actions
ORD-001John Doe[email protected]Premium Widget$299.99delivered1/15/2024North AmericaNew YorkCredit CardExpressLeave at front desk
ORD-002Jane Smith[email protected]Basic Kit$149.50shipped1/18/2024EuropeLondonPayPalStandardGift wrap requested
ORD-003Bob Johnson[email protected]Pro Bundle$599.00pending1/20/2024Asia PacificTokyoBank TransferEconomyCall before delivery
ORD-004Alice Williams[email protected]Starter Pack$79.99delivered1/22/2024North AmericaChicagoCredit CardStandardNo special instructions
ORD-005Charlie Brown[email protected]Enterprise Suite$1,299.00shipped1/25/2024EuropeBerlinInvoiceExpressBusiness address only
ORD-006Diana Prince[email protected]Premium Widget$299.99cancelled1/28/2024Asia PacificSydneyCredit CardStandardCustomer cancelled
ORD-007Ethan Hunt[email protected]Basic Kit$149.50pending2/1/2024North AmericaLos AngelesPayPalExpressSignature required
ORD-008Fiona Green[email protected]Pro Bundle$599.00delivered2/5/2024EuropeParisCredit CardStandardApartment 4B
ORD-009George Miller[email protected]Starter Pack$79.99shipped2/8/2024Asia PacificSingaporeBank TransferEconomyOffice hours only
ORD-010Hannah Lee[email protected]Enterprise Suite$1,299.00delivered2/12/2024North AmericaSeattleInvoiceExpressInclude packing slip
Current Table State
Live view of the current table state for demonstration purposes
Search Query:None
Total Items:10
Sorting:None
Page:1 (Size: 10)
Hidden Columns:0
Resized Columns:None (using default sizes)
View Full State Object
Sorting:
[]
Column Visibility:
{}
Column Sizing:
{}

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.

Install the DataTable core and add-ons for this example:

pnpm dlx shadcn@latest add @niko-table/data-table @niko-table/data-table-column-resize @niko-table/data-table-column-sort @niko-table/data-table-column-hide @niko-table/data-table-pagination @niko-table/data-table-search-filter @niko-table/data-table-view-menu

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.

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",
},
// ...
]

Drop <DataTableColumnResize /> early inside DataTableRoot. Its presence flips enableColumnResizing via feature detection: no config flag required.

column-resize.tsx
<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.

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

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):

columns.tsx
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 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:

columns.tsx
// 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:

wide-table.tsx
<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.

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:

pnpm dlx shadcn@latest add @niko-table/data-table-column-auto-fit
auto-fit.tsx
<DataTableRoot data={data} columns={columns}>
<DataTableColumnResize />
<DataTableColumnAutoFit />
<DataTable>
<DataTableHeader />
<DataTableBody />
</DataTable>
</DataTableRoot>

These columns declare small size values yet fill the width:

Open in
Team
City
Wins
Status
Fountain Valley HornetsHalifax21active
Darefield LightningHalifax9active
South Jordan CougarsHalifax11active
Treutelside PhoenixHalifax18active
West Lila GrizzliesHalifax7inactive
Declared width total:480px
Auto-fit width total:measuring

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:

column-resize-state.tsx
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>
)
}

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:

Open in

Resize a column, then reload. Widths are restored from localStorage. On first load, before anything is saved, the columns fill the container width.

Order ID
Customer
Email
Product
Status
ORD-001John Doe[email protected]Premium Widgetdelivered
ORD-002Jane Smith[email protected]Basic Kitshipped
ORD-003Bob Johnson[email protected]Pro Bundlepending
ORD-004Alice Williams[email protected]Starter Packdelivered
ORD-005Charlie Brown[email protected]Enterprise Suiteshipped

No widths saved yet. Showing the default fill layout.

pnpm dlx shadcn@latest add @niko-table/data-table-column-sizing-persistence
orders-table.tsx
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.

✅ 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 /> on DataTableHeader + 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

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.