Skip to main content

Columns

Resize by dragging a column's edge, and reorder, pin, or show/hide columns — either by dragging headers directly or from the Columns panel in the toolbar. Pinned columns are excluded from reordering.

Loading demo…
<DataTable
columns={columns}
data={data}
enableColumnResizing
enableColumnReordering
enableColumnPinning
enableColumnVisibility
initialState={{ columnPinning: { left: ['name'] } }}
/>

Props

PropTypeDefaultDescription
enableColumnResizingbooleanfalseDrag column edges to resize.
columnResizeMode'onChange' | 'onEnd''onChange'Apply resize live or on release.
enableColumnReorderingbooleanfalseDrag headers — or rows in the Columns panel — to reorder.
onColumnOrderChange(order) => voidColumns reordered.
enableColumnPinningbooleanfalseSticky left/right columns.
enableColumnVisibilitybooleantrueShow/hide (eye toggle) from the Columns panel.
enableColumnMenubooleantruePer-column header ⋮ menu (sort/hide/autosize).

Column menu

Each column header has a quiet menu (revealed on hover, or when the header is focused) that surfaces the actions already wired into the grid — so users discover them without hunting:

  • Sort ascending / descending / clear — when the column is sortable (enableSorting). The current direction is marked; redundant items are disabled. Sorting from the menu sorts by that single column (replacing any shift-click multi-sort); Clear sort removes only this column's sort, leaving the rest.
  • Autosize this column — when enableColumnResizing is on (same as double-clicking the resize handle).
  • Hide column — when enableColumnVisibility is on (disabled for the last visible column).

The menu only shows items that are actually available; if none are, no kebab renders. It's on by default — set enableColumnMenu={false} to remove it table-wide, or disableColumnMenu: true on a single column. Keyboard: focus a header cell and press Enter to open its menu. Swap the glyph with slots.columnMenuIcon.

Loading demo…
<DataTable
columns={columns}
data={data}
enableColumnMenu // default true
enableColumnResizing // unlocks "Autosize this column"
enableColumnVisibility // unlocks "Hide column"
/>

The Columns panel

The toolbar's Columns button (shown when any of enableColumnVisibility, enableColumnPinning, or enableColumnReordering is set) opens a panel to manage every column in one place:

  • Show / hide — the eye toggle per column (greyed when hidden).
  • Pin left / right — when enableColumnPinning is on.
  • Reorder — drag the handle (⠿) to reposition, when enableColumnReordering is on.
  • Show all · Reset · Done in the footer.

Changes apply live. The built-in selection / expander columns are excluded.

Grouped headers

Nest columns under a parent with columns: [...] to get a multi-level header. The group header spans its leaf columns and stays aligned with them and the body (group headers aren't sortable/resizable and have no ⋮ menu). Grouped grids hold each column's width (so the group sums its leaves exactly) and scroll rather than stretch.

Loading demo…
const columns = [
{ id: 'name', header: 'Name', accessorKey: 'name' },
{ id: 'contact', header: 'Contact', columns: [
{ id: 'email', header: 'Email', accessorKey: 'email' },
{ id: 'role', header: 'Role', accessorKey: 'role' },
] },
{ id: 'status', header: 'Status', accessorKey: 'status' },
];

Column sizing

Set size, minSize, and maxSize on a ColumnDef. Because the grid uses CSS Grid (not an HTML <table>), min/max are respected and resizing one column never shifts its neighbours. Use fitToScreen to stretch columns to fill the width while still honouring min/max.

With enableColumnResizing, double-click a column's resize handle to auto-fit it to its content (clamped to minSize/maxSize). The same is available imperatively via apiRef.current.columnResizingautoSizeColumn(id) and autoSizeAllColumns(). Auto-fit measures the rows currently rendered (the virtual window / current page).

Cell & row styling

By default a cell truncates overflowing text with an ellipsis. Set wrapText on a column to wrap instead — in the non-virtualized grid the row grows to fit. You can also attach CSS classes conditionally: per-column with cellClassName / headerClassName, or table-wide with getRowClassName / getCellClassName (the table-level cell hook merges with the per-column one).

Loading demo…
const columns = [
{ id: 'role', header: 'Role', accessorKey: 'role',
headerClassName: 'role-header',
cellClassName: ({ value }) => (value === 'Admin' ? 'is-admin' : '') },
// Wrap long text instead of truncating (the row grows to fit).
{ id: 'bio', header: 'Bio', accessorKey: 'bio', wrapText: true },
];

<DataTable
columns={columns}
data={data}
getRowClassName={({ row, index }) => (index % 2 ? 'odd-row' : '')}
getCellClassName={({ columnId, value }) => (value == null ? 'is-empty' : '')}
/>
OptionTypeScopeDescription
wrapTextbooleancolumnWrap cell + header text instead of ellipsis truncation.
cellClassNamestring | (({ value, row }) => string)columnClass for each body cell of the column.
headerClassNamestringcolumnClass for the column's header cell.
getRowClassName({ row, index }) => stringtableClass for each row.
getCellClassName({ row, columnId, value }) => stringtableClass for each cell (merges with cellClassName).

:::note Virtualization wrapText grows rows in the standard grid. Under enableVirtualization the row height is a fixed estimate, so wrapped content is clipped — per-row measurement for virtualized auto-height is a separate enhancement. :::

Value getters & formatters

Give a column a type and it renders a sensible default cell — no hand-written renderers:

  • type: 'number' → locale grouping (1,234.5)
  • type: 'date'Jun 12, 2026 (parsed with dayjs, no UTC drift)
  • type: 'boolean'Yes / No
  • type: 'select' → the matching options label

For a computed column, use valueGetter (it compiles to an accessor, so sorting, filtering, and export all read the same value). For display-only formatting, use valueFormatter — it never affects export. Precedence for the cell: an explicit cell > valueFormatter > the type default > the raw value.

Loading demo…
const columns = [
// computed column (no accessorKey) — drives sort/filter/export too
{ id: 'name', header: 'Rep', valueGetter: ({ row }) => `${row.first} ${row.last}` },
// type default + a display-only override
{ id: 'amount', header: 'Amount', accessorKey: 'amount', type: 'number',
valueFormatter: ({ value }) => `$${Number(value).toLocaleString()}` },
{ id: 'closedOn', header: 'Closed', accessorKey: 'closedOn', type: 'date' },
{ id: 'won', header: 'Won', accessorKey: 'won', type: 'boolean' },
];

Row actions

getRowActions(row) adds an auto-generated _actions column (pinned right when enableColumnPinning is on, like the selection/expander columns; otherwise the last column). A few actions render as inline icon buttons; returning 3+ (or any action without an icon) collapses them into an overflow menu. Every handler stops propagation, so it won't trigger onRowClick.

Loading demo…
import EditOutlined from '@mui/icons-material/EditOutlined';
import DeleteOutline from '@mui/icons-material/DeleteOutline';

<DataTable
columns={columns}
data={data}
getRowActions={(row) => [
{ label: 'Edit', icon: EditOutlined, onClick: () => edit(row.original) },
{ label: 'Delete', icon: DeleteOutline, color: 'error',
disabled: row.original.locked,
onClick: () => remove(row.original) },
]}
/>

Each action: { label, icon?, onClick(row), disabled?, hidden?, color? }. Tune the column via slotProps.actionsColumn, force the layout with rowActionsDisplay ('icons' | 'menu' | 'auto'), and swap the overflow glyph with slots.moreActionsIcon.