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.
<DataTable
columns={columns}
data={data}
enableColumnResizing
enableColumnReordering
enableColumnPinning
enableColumnVisibility
initialState={{ columnPinning: { left: ['name'] } }}
/>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
enableColumnResizing | boolean | false | Drag column edges to resize. |
columnResizeMode | 'onChange' | 'onEnd' | 'onChange' | Apply resize live or on release. |
enableColumnReordering | boolean | false | Drag headers — or rows in the Columns panel — to reorder. |
onColumnOrderChange | (order) => void | — | Columns reordered. |
enableColumnPinning | boolean | false | Sticky left/right columns. |
enableColumnVisibility | boolean | true | Show/hide (eye toggle) from the Columns panel. |
enableColumnMenu | boolean | true | Per-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
enableColumnResizingis on (same as double-clicking the resize handle). - Hide column — when
enableColumnVisibilityis 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.
<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
enableColumnPinningis on. - Reorder — drag the handle (⠿) to reposition, when
enableColumnReorderingis 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.
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.columnResizing
— autoSizeColumn(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).
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' : '')}
/>
| Option | Type | Scope | Description |
|---|---|---|---|
wrapText | boolean | column | Wrap cell + header text instead of ellipsis truncation. |
cellClassName | string | (({ value, row }) => string) | column | Class for each body cell of the column. |
headerClassName | string | column | Class for the column's header cell. |
getRowClassName | ({ row, index }) => string | table | Class for each row. |
getCellClassName | ({ row, columnId, value }) => string | table | Class 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 withdayjs, no UTC drift)type: 'boolean'→Yes/Notype: 'select'→ the matchingoptionslabel
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.
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.
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.