Columns
Metadata, sizing, and cells
Columns are ColumnDef<TData>[] imported from ng-advanced-table. Put product-specific rendering in your column definitions and keep table-wide wiring in the surface.
Basic Column Shape
Use accessorKey for fields and id for display or computed columns. Provide meta.label for every column.
import { type ColumnDef } from 'ng-advanced-table';
interface PositionRow {
id: string;
symbol: string;
company: string;
price: number;
changePercent: number;
}
readonly columns: ColumnDef<PositionRow>[] = [
{
accessorKey: 'symbol',
header: 'Symbol',
meta: { label: 'Symbol', rowHeader: true },
},
{
accessorKey: 'company',
header: 'Company',
meta: { label: 'Company', cellMaxLines: 2 },
},
{
accessorKey: 'price',
header: 'Last',
meta: { label: 'Last', align: 'end' },
cell: (context) => currency.format(context.getValue<number>()),
},
];
Column Metadata
NatTableColumnMeta adds table-specific behavior to columnDef.meta.
| Field | Use it for |
|---|---|
label |
Stable human-readable label for announcements and companion UI |
hiddenHeaderLabel |
Screen-reader-only header text for compact utility columns |
align |
'start' or 'end' header and body alignment |
rowHeader |
Marks body cells in the column as row headers |
reorderable |
Per-column reordering override; false blocks grabbing/moving this column, while true opts it into reordering (drag, keyboard, menu) when the surface is off |
cellTone |
Semantic tone class for positive, negative, neutral, or warning cells |
cellHeight |
Fixed body-cell height for this column |
cellMaxLines |
Body-cell line clamp; defaults to 2, use Infinity to disable |
headerSize |
Header-only width |
headerMinSize |
Header-only minimum width |
headerMaxSize |
Header-only maximum width |
export |
Export participation, header, and value mapping |
import type { NatTableColumnMeta } from 'ng-advanced-table';
type PositionColumnMeta = NatTableColumnMeta<PositionRow>;
const priceMeta: PositionColumnMeta = {
label: 'Last',
align: 'end',
cellTone: (context) => {
const change = context.row.original.changePercent;
if (change > 0) return 'positive';
if (change < 0) return 'negative';
return 'neutral';
}
};
Sizing
Use size, minSize, and maxSize for body-cell sizing. Header-only sizing lives in metadata.
readonly columns: ColumnDef<PositionRow>[] = [
{
accessorKey: 'symbol',
header: 'Symbol',
size: 112,
minSize: 96,
meta: {
label: 'Symbol',
rowHeader: true,
headerSize: 132,
},
},
{
accessorKey: 'company',
header: 'Company',
minSize: 180,
maxSize: 360,
meta: {
label: 'Company',
cellHeight: 64,
cellMaxLines: 2,
},
},
];
Configure the table width model on the surface.
<nat-table-surface columnSizingMode="fixed" columnResizeMode="onEnd" [enableColumnResizing]="true">
<nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" />
<nat-table-scroll-control />
</nat-table-surface>
columnSizingMode="fill" stretches columns to fill the container. columnSizingMode="fixed" treats widths as authoritative and lets the table scroll horizontally. Interactive resizing is off until the surface sets [enableColumnResizing]="true"; once enabled it applies to every column, and a single column opts out with enableResizing: false. columnResizeMode="onEnd" commits after pointer release; "onChange" updates during the drag.
Pinning And Reordering
Pinning is enabled where the column allows it. Reordering is disabled by default. Set [enableReordering]="true" to enable drag/drop, keyboard moves, and companion move actions for every column, or leave it off and set meta: { reorderable: true } on only the columns that should move. Reordering stays inside the current pinning zone: left, center, or right.
With surface reordering enabled, every column is reorderable by default. Opt a column out with meta: { reorderable: false } to remove its drag, keyboard, and header-menu move affordances. The column is not itself grabbed, but another reorderable column can still move past it and displace it.
readonly initialState: Partial<NatTableUserState> = {
columnPinning: {
left: ['symbol'],
right: ['actions'],
},
columnOrder: ['symbol', 'company', 'price', 'changePercent', 'actions'],
};
With surface reordering enabled, columns in baseColumns reorder by default; add meta: { reorderable: false } to any column that should not be grabbed or moved directly.
const baseColumns: ColumnDef<PositionRow>[] = [
{ accessorKey: 'symbol', header: 'Symbol', meta: { label: 'Symbol', rowHeader: true } },
{ accessorKey: 'company', header: 'Company', meta: { label: 'Company' } },
// Cannot be grabbed or moved directly.
{ accessorKey: 'price', header: 'Last', meta: { label: 'Last', align: 'end', reorderable: false } },
];
readonly columns = withNatTableHeaderActions(baseColumns, {
enableColumnReorderActions: true,
});
The header actions helper adds sort buttons when the surface enables sorting ([enableSorting]="true") and a column can sort, pin menu items when the surface enables pinning ([enablePinning]="true") and the column can pin, and move menu items when enableColumnReorderActions is on and column.meta.reorderable ?? surface.enableReordering resolves to true. Sorting, pinning, and resizing resolve per column as column.<flag> ?? surface.<enabler>; reordering uses column.meta.reorderable ?? surface.enableReordering. It also accepts custom sort indicator content through sortIndicator, so custom sort icons should compose through the helper rather than through extra header rows.
Use the surface flag for table-wide reordering.
<nat-table-surface [enableReordering]="true">
<nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" />
</nat-table-surface>
Custom Cell Components
Use flexRenderComponent(...) when a cell should render an Angular component. Keep data loading, mutations, and dialogs in the container; the cell component should emit intent.
import { Component, input, output } from '@angular/core';
import { GridCellWidget } from '@angular/aria/grid';
import { flexRenderComponent, type ColumnDef } from 'ng-advanced-table';
@Component({
selector: 'app-position-actions-cell',
imports: [GridCellWidget],
template: `
<button
type="button"
ngGridCellWidget
[attr.aria-label]="'Open actions for ' + symbol()"
(click)="opened.emit()"
>
Actions
</button>
`,
})
export class PositionActionsCell {
readonly symbol = input.required<string>();
readonly opened = output<void>();
}
readonly columns: ColumnDef<PositionRow>[] = [
{
accessorKey: 'symbol',
header: 'Symbol',
meta: { label: 'Symbol', rowHeader: true },
},
{
id: 'actions',
header: 'Actions',
enableSorting: false,
enableGlobalFilter: false,
enableHiding: false,
meta: {
label: 'Actions',
hiddenHeaderLabel: 'Row actions',
align: 'end',
cellMaxLines: Infinity,
},
cell: (context) =>
flexRenderComponent(PositionActionsCell, {
inputs: { symbol: context.row.original.symbol },
outputs: { opened: () => this.openActions(context.row.original.id) },
}),
},
];
Put ngGridCellWidget on the real focusable element. If a design-system component renders the button internally, apply the widget marker inside that component.
Row Activation
Use (rowActivate) for row-level navigation or primary actions. It fires on a primary click or Enter / Space, but ignores activations that start inside interactive descendants.
<nat-table [data]="rows()" [columns]="columns" accessibleName="Open positions" (rowActivate)="openPosition($event.rowData.id)" />
protected openPosition(positionId: string): void {
this.router.navigate(['/positions', positionId]);
}
Keep cell-level buttons, links, menus, and inputs independent. They should emit their own outputs and should not depend on row activation.
Opting A Column Out Of Row Activation
The interactive-descendant guard only covers the control itself. The cell padding around a small action button is still part of the row target, so a click that misses the button by a few pixels navigates away. WCAG 2.5.8 (Target Size, AA) also requires a 24 × 24 CSS px target or that much clearance from other targets, and the surrounding row counts as another target. Set meta.rowActivation: false on such columns so the whole cell is excluded:
{
id: 'actions',
header: 'Actions',
meta: { label: 'Actions', hiddenHeaderLabel: 'Row actions', rowActivation: false },
cell: (context) => flexRenderComponent(PositionActionsCell, { inputs: { symbol: context.row.original.symbol } }),
}
Clicks and Enter / Space that start anywhere inside that cell no longer emit rowActivate; every other cell in the row still does. The renderer stamps data-nat-row-activation="false" on the cell, and you can place that attribute on any element inside a cell template for a finer opt-out. NatStaticTable stamps it too. NatList honors the flag on its fields for pointer clicks; keyboard focus in a list sits on the whole item, so Enter/Space keep activating until every visible column opts out, at which point the item stops activating entirely (see the list renderer topic).
Export Metadata
Accessor columns export by default. Display columns opt out unless explicitly enabled. Use meta.export when exported data should differ from rendered text.
{
accessorKey: 'price',
header: 'Last',
meta: {
label: 'Last',
align: 'end',
export: {
header: 'Last price',
value: ({ value }) => Number(value),
},
},
cell: (context) => currency.format(context.getValue<number>()),
}
Disable export for columns that are useful only on screen.
{
id: 'actions',
header: 'Actions',
enableSorting: false,
enableGlobalFilter: false,
meta: {
label: 'Actions',
export: { enabled: false },
},
cell: (context) => renderActions(context.row.original),
}
Common Mistakes
- Do not omit
meta.labelbecause the visibleheaderhappens to be a string today. - Do not rely on namespaced positional fallback ids for interactive tables.
- Do not make clickable
<div>or<span>content inside cells; use real controls. - Do not add fake header rows to customize sorting; use
withNatTableHeaderActions(..., { sortIndicator }). - Do not leave resizing enabled on columns where it is not useful; the surface enables it for all columns, so opt individual ones out with
enableResizing: false. - Do not move workflow state such as filters, dialogs, or mutations into cell components.