Skip to main content

Columns

Column definitions describe identity, value access, formatting, interaction, and presentation. The same definition model is used by the core and both framework bindings.

Loading interactive example…
React
const columns: ReactColDef[] = [
{ key: "orderNo", label: "Order", pinned: "left" },
{ key: "customer", label: "Customer", width: 180 },
{ key: "revenue", label: "Revenue", type: ColumnType.CURRENCY },
];

<Grid
rowData={rows}
columnDefs={columns}
rowNumbers
columnPanel={{ trigger: "toolbar" }}
toolbar={{ quickFilter: true }}
/>

Essential fields​

FieldPurpose
keyReads a field from row data and provides a stable fallback identity
colIdExplicit public identity for state and API calls
labelVisible header label
typeSelects default comparison, formatting, filtering, and editing behavior
valueGetterComputes a value instead of reading key; receives the row node (node.data is the row)
valueFormatter / valueParserConverts values for display and committed edits

Users can resize and reorder enabled columns directly. Initial pinning and visibility use pinned and hidden; the API and column panel can change both at runtime. Capture the resulting layout with api.getColumnState() and restore it with api.applyColumnState(state).

Shared behavior

Put common capabilities such as sortable, resizable, and filter in defaultColDef. An explicit column value always wins.

Column types and formatting​

import { ColumnType, type ColDef } from "@agility-workbench/grid";

const columnDefs: ColDef[] = [
{ key: "name", label: "Name", type: ColumnType.STRING },
{ key: "quantity", label: "Quantity", type: ColumnType.NUMBER },
{ key: "active", label: "Active", type: ColumnType.BOOLEAN },
{ key: "createdAt", label: "Created", type: ColumnType.DATE },
{ key: "revenue", label: "Revenue", type: ColumnType.CURRENCY },
];

Value getter, formatter, and parser​

const column = {
colId: "total",
label: "Total",
valueGetter: (node) => node.data.quantity * node.data.unitPrice,
valueFormatter: ({ value }) => `$${Number(value).toFixed(2)}`,
editable: true,
valueParser: ({ value }) => Number(value),
} satisfies ColDef;

Pinning, visibility, and capabilities​

const columnDefs: ColDef[] = [
{ key: "id", label: "ID", pinned: "left", movable: false },
{ key: "internal", label: "Internal", hidden: true },
{ key: "name", label: "Name", hideable: false },
{ key: "actions", label: "Actions", pinned: "right", resizable: false },
];

Users pin and hide columns through the column menu or the column panel. The API does the same through typed methods, each emitting the columnsChanged the gesture would (with source: "api"):

api.setColumnPinned("id", "left"); // or ["id", "name"]; null unpins
api.setColumnsVisible(["internal", "notes"], false);
api.setColumnVisible("internal", true);

// Indexes are positions in api.getColumnState() order: every non-internal leaf,
// hidden ones included, left section, then centre, then right.
api.moveColumn("name", 0);
api.moveColumns(["price", "qty"], 3); // a block, in the order given

Hiding skips hideable: false columns (showing one always works); pinning skips columns that cannot be pinned. A move keeps each column in its own section — the index is clamped to that section's range, so pin first to move a column across sections — and skips movable: false and lockPosition columns. A column inside a group leaves the group when the target is outside it, as a header drag does; pass the group's colId to move the whole group.

Locking a column's position​

const columnDefs: ColDef[] = [
{ key: "id", label: "ID", lockPosition: "left" }, // or true
{ key: "name", label: "Name" },
{ key: "amount", label: "Amount" },
{ key: "actions", label: "Actions", lockPosition: "right" },
];

lockPosition holds a column at an edge of its section: true or "left" at the left (after the selection-checkbox column), "right" at the right. A locked column cannot be dragged, moved from the column panel or moved with api.moveColumn, and a column dropped next to it lands beside it rather than displacing it. applyColumnState, saved views and setColumnDefs put locked columns back at their edge, whatever order they carry. Pinning still applies: a locked column pinned left sits at the edge of the left section. Inside a column group the lock holds the column at its group's edge; set lockPosition on the group definition to lock the whole group.

Explicit width and automatic sizing​

const options = {
minResizeWidth: 60,
maxColumnWidth: 360,
columnDefs: [
{ colId: "name", key: "name", label: "Name", minWidth: 120, maxWidth: 280 },
{ key: "notes", label: "Notes", width: 240 },
],
} satisfies GridOptions;

// Fit columns to their rendered content (one columnWidthsChanged for the batch).
api.autoSizeColumns(["name", "notes"]);
api.autoSizeAllColumns();

// Set widths in pixels, as a resize-handle drag would.
api.setColumnWidth("name", 180);
api.setColumnWidths([
{ colId: "name", widthPx: 180 },
{ colId: "notes", widthPx: 320 },
]);

// Stretch or shrink the unpinned columns to fill the centre viewport.
api.sizeColumnsToFit();

A declared width is the column's width: the grid does not resize it to fit the data, only clamps it to the column's own minWidth / maxWidth. A column without a width is sized to its header and content on the first data load, floored at minResizeWidth and capped at maxColumnWidth (a column's own minWidth / maxWidth take precedence over those two). Once a user drags a column edge, that width wins until an explicit fit-to-content: double-clicking the resize handle or calling autoSizeColumns / autoSizeAllColumns measures content even for a column with a declared width. Widths set with setColumnWidth(s) behave like a drag: clamped to the column's bounds and kept across later autosizes.

sizeColumnsToFit() fits the centre section only: the visible unpinned columns are scaled together, keeping their relative widths, so they exactly fill the centre viewport. Pinned sections keep their widths (they are width-clamped and never scroll), as do resizable: false columns and the row-number and checkbox columns. Each fitted column stays within its minWidth / maxWidth (falling back to minResizeWidth / maxColumnWidth), so when those bounds cannot reach the viewport the fit falls short or overflows. The new widths count as user widths and arrive as one columnWidthsChanged. Call it after the grid is laid out — before that it does nothing.

Set autosizeColumnsOnDataChange: true (the default for the server-side row model) to recompute content widths after each data refresh; declared and user-resized widths are kept. Enable a leading row-number column with rowNumbers: true.

Cell spanning​

const columnDefs: ColDef[] = [
{
key: "title",
label: "Title",
colSpan: ({ data }) => data.kind === "section" ? 3 : 1,
},
{ key: "owner", label: "Owner" },
{ key: "status", label: "Status" },
];

A span stops at a pinned-section boundary.

Conditional cell styling​

const column = {
key: "change",
label: "Change",
cellClass: ({ value }) => Number(value) < 0 ? "is-negative" : "is-positive",
cellStyle: ({ value }) => ({ fontWeight: Number(value) > 10 ? "700" : "400" }),
} satisfies ColDef;

Header content and whole-cell components​

const labelOnly = ({ displayName }) => {
const strong = document.createElement("strong");
strong.textContent = displayName;
return strong;
};

const columnDefs: ColDef[] = [
{ key: "name", label: "Name", headerComponent: labelOnly },
{ key: "status", label: "Status", headerCellComponent: labelOnly },
];

headerComponent replaces header content while preserving grid controls. headerCellComponent replaces the whole header cell except its resize handle. The framework bindings accept React/Angular components in both slots.

Wrapping header text and auto header height​

const options = {
defaultColDef: { wrapHeaderText: true, autoHeaderHeight: true },
columnDefs: [
{
label: "Commercial performance this fiscal year",
children: [
{ key: "units", label: "Units shipped to date across all warehouses", width: 120 },
{ key: "revenue", label: "Recognised revenue in reporting currency", width: 120 },
],
},
],
} satisfies GridOptions;

wrapHeaderText lets a long label break onto several lines instead of being cut off with an ellipsis; the sort icon, menu button and group expander keep their place beside it. autoHeaderHeight grows the header level that label sits on until the whole label fits: the level becomes as tall as its tallest such label (plus the cell's padding), and never shorter than the configured headerHeight / leafHeaderHeight / parentHeaderHeight. Leaf and group levels grow independently (put the flags on a group definition, or in defaultColDef, for the group level), and the pinned sections always grow together. The header re-measures when column widths settle (a resize, autosize or fit), when columns are shown, hidden, moved or pinned, when definitions or labels change, when the header heights or the theme change, and when a web font finishes loading.

Without autoHeaderHeight, a wrapped label is clipped at the configured header height — set a taller headerHeight yourself, or turn auto height on.

Column state​

const savedState = api.getColumnState();

// Later: restore widths, order, pinning, and visibility.
api.applyColumnState(savedState);

// Exact restore: hide columns absent from the saved state.
api.applyColumnState(savedState, { defaultState: { hidden: true } });

Column state carries each column's sort too (sort, null when unsorted, and sortIndex); see Sorting.

Add or replace definitions at runtime​

api.setColumnDefs([...baseColumns, { key: "margin", label: "Margin" }]);

api.addSparklineColumn({
colIds: ["jan", "feb", "mar"],
sparklineType: "line",
targetColId: "revenue",
});

api.getColumnDefs() returns the definitions as the grid holds them now, with the current widths, pinning, visibility, sort and aggregates written in, so api.setColumnDefs(api.getColumnDefs()) changes nothing. Edit the result and set it back to add or change one column without rebuilding the list by hand. For a single column's live object, api.getColumn(colId); for every leaf in display order, api.getColumns().