Columns
A column is a plain object (ColumnDef<T> from @dzgrid/core). Only the keys described here exist; TypeScript
flags an unknown key, and the grid ignores it. Angular’s DzgColumnDef<T> is the same object, and also takes
component classes in ui. Each key has its own page under Column options; this guide shows
how they work together.
Value and type
Section titled “Value and type”import { createGrid } from '@dzgrid/dom';import type { ColumnOrGroup } from '@dzgrid/core';import { textCodec } from '@dzgrid/core';import { EN } from '@dzgrid/dom/locales/en';
type Parcel = { id: number; code: string; sender: { name: string; town: string }; status: string; weight: number; sentOn: string; arrivedAt: number | null; insured: boolean;};
const shout = { ...textCodec, format: (v: string) => v.toUpperCase() };
const columns: ColumnOrGroup<Parcel>[] = [ { id: 'code', header: 'Code', width: 110, pinned: 'start', codec: shout }, { id: 'sender', header: 'Sender', field: 'sender.name', flex: 2, minWidth: 160 }, { id: 'status', header: 'Status', type: 'enum', enumOptions: [{ value: 'open', label: 'Open' }, { value: 'done', label: 'Delivered' }] }, { id: 'weight', header: 'Weight', type: 'number', align: 'end', numberFormat: { maximumFractionDigits: 2 } }, { id: 'sentOn', header: 'Sent on', type: 'day' }, { id: 'arrivedAt', header: 'Arrived', type: 'instant', instantFormat: { date: true, time: true } }, { id: 'insured', header: 'Insured', type: 'boolean' }, { id: 'heavy', header: 'Heavy', type: 'boolean', value: (p) => p.weight > 20, sortable: false, filter: false },];
createGrid<Parcel>(document.getElementById('parcels')!, { rows: [{ id: 1, code: 'px-104', sender: { name: 'Orla Penhallow', town: 'Wrenmouth' }, status: 'open', weight: 2.4, sentOn: '2026-03-02', arrivedAt: null, insured: true }], columns, getRowId: (p) => p.id, messages: EN, timeZone: 'Europe/London',});import { Component } from '@angular/core';import { DzGrid, type DzgColumnOrGroup } from '@dzgrid/angular';import { EN } from '@dzgrid/dom/locales/en';
type Parcel = { id: number; code: string; sender: { name: string }; status: string; weight: number; sentOn: string; insured: boolean };
@Component({ selector: 'app-parcels', imports: [DzGrid], template: `<dz-grid [rows]="rows" [columns]="columns" [getRowId]="rowId" [messages]="messages" timeZone="Europe/London" />`,})export class Parcels { readonly rows: Parcel[] = [{ id: 1, code: 'px-104', sender: { name: 'Orla Penhallow' }, status: 'open', weight: 2.4, sentOn: '2026-03-02', insured: true }]; readonly columns: DzgColumnOrGroup<Parcel>[] = [ { id: 'code', header: 'Code', width: 110, pinned: 'start' }, { id: 'sender', header: 'Sender', field: 'sender.name', flex: 2, minWidth: 160 }, { id: 'status', header: 'Status', type: 'enum', enumOptions: [{ value: 'open', label: 'Open' }, { value: 'done', label: 'Delivered' }] }, { id: 'weight', header: 'Weight', type: 'number', align: 'end', numberFormat: { maximumFractionDigits: 2 } }, { id: 'sentOn', header: 'Sent on', type: 'day' }, { id: 'insured', header: 'Insured', type: 'boolean' }, { id: 'heavy', header: 'Heavy', type: 'boolean', value: (p) => p.weight > 20, sortable: false, filter: false }, ]; readonly rowId = (p: Parcel) => p.id; readonly messages = EN;}import '@dzgrid/element/define';import type { DzGridElement } from '@dzgrid/element';import { EN } from '@dzgrid/dom/locales/en';
type Parcel = { id: number; code: string; sender: { name: string }; status: string; weight: number; sentOn: string; insured: boolean };
const grid = document.querySelector<DzGridElement<Parcel>>('dz-grid')!;grid.columns = [ { id: 'code', header: 'Code', width: 110, pinned: 'start' }, { id: 'sender', header: 'Sender', field: 'sender.name', flex: 2, minWidth: 160 }, { id: 'status', header: 'Status', type: 'enum', enumOptions: [{ value: 'open', label: 'Open' }, { value: 'done', label: 'Delivered' }] }, { id: 'weight', header: 'Weight', type: 'number', align: 'end', numberFormat: { maximumFractionDigits: 2 } }, { id: 'sentOn', header: 'Sent on', type: 'day' }, { id: 'insured', header: 'Insured', type: 'boolean' }, { id: 'heavy', header: 'Heavy', type: 'boolean', value: (p) => p.weight > 20, sortable: false, filter: false },];grid.getRowId = (p) => p.id;grid.messages = EN;grid.setAttribute('time-zone', 'Europe/London');grid.rows = [{ id: 1, code: 'px-104', sender: { name: 'Orla Penhallow' }, status: 'open', weight: 2.4, sentOn: '2026-03-02', insured: true }];Drag a title to move its column, drag its edge to resize; the ⋮ button on a title opens its menu.
idis required, unique and stable. The value is read fromfield(a dotted path, own properties only; by default theid), or computed byvalue(row).type:text(default),number,day,instant,boolean,enum,custom.- A
dayis a'YYYY-MM-DD'string. Aninstantis epoch milliseconds or ISO text with an offset (Z,+02:00). Text with no offset is ambiguous: the cell is blank and anambiguous-instantproblem is reported, unless the column says which zone to read it in withassumeZone: 'Europe/Paris'. A JavaScriptDateis not read: passdate.getTime(). - What a cell shows comes from the type and
numberFormat,instantFormat,enumOptions, or a wholecodec(read, format, sort). Instants are shown in the grid’stimeZone.
Size and place
Section titled “Size and place”width, minWidth, maxWidth (px), flex (a share of the room left), resizable, movable, pinned: 'start' | 'end', hidden, align: 'start' | 'center' | 'end', wrap (several lines; rows grow with the autoHeight()
feature from @dzgrid/dom/auto-height). Sides are logical: start is the left in English and the right in Arabic.
Filling the width: there is no separate « fit to width » switch. Give the columns that should stretch a flex
(and a minWidth, so they never get too narrow): they share the room the fixed columns leave, and follow the grid’s
width when the window is resized. With every column flexible, the columns always fill the grid exactly.
Fitted to their content at load: columns({ fitOnLoad: 'content' }) from @dzgrid/dom/columns fits the shown
columns once, when the first rows are shown (on a server, the first page): header and cells as drawn, within
minWidth / maxWidth. Flexible columns are left alone (they share what remains), and so is any width already set (a
restored state, the user). Data changes never refit; columnsFeature.api(grid).fitContent() does it again on your
call, never over a width the user set.
Columns made from the data
Section titled “Columns made from the data”autoColumns(rows, options) from @dzgrid/core/auto-columns makes the column list from the first rows: one column per
field, typed by its values — numbers, yes / no, YYYY-MM-DD days, ISO date-times with an offset as instants (one
without an offset stays text: its zone is never guessed), else text. It returns { columns, problems }.
import { autoColumns } from '@dzgrid/core/auto-columns';import type { ColumnDef } from '@dzgrid/core';import { createGrid } from '@dzgrid/dom';import { EN } from '@dzgrid/dom/locales/en';
type Booking = { ref: string; room: string; guests: number; paid: boolean; from: string; to: string; note: string };const rows: Booking[] = [ { ref: 'B-1', room: 'Lagoon', guests: 2, paid: true, from: '2026-07-01T14:00:00Z', to: '2026-07-04T10:00:00Z', note: '' }, { ref: 'B-2', room: 'Cliff', guests: 4, paid: false, from: '2026-07-02T14:00:00Z', to: '2026-07-09T10:00:00Z', note: 'Late arrival' },];const labels: Record<string, string> = { ref: 'Ref.', room: 'Room', guests: 'Guests', paid: 'Paid', stay: 'Stay' };const actions: ColumnDef<Booking> = { id: 'actions', header: 'Actions', sortable: false, filter: false };
const { columns, problems } = autoColumns(rows, { header: (field) => labels[field] ?? field, // translation override: { guests: { width: 90 } }, // any part of a definition wins over the guess merge: { stay: { from: ['from', 'to'], flex: 1 } }, // one column reading both fields; they are hidden hide: ['note'], // made, hidden (the columns chooser can show it) add: [actions], // columns absent from the data order: ['room', 'stay'], // these first, the rest in the data's order});if (problems.length) console.warn(problems); // a name at most, never a value
createGrid<Booking>(document.getElementById('bookings')!, { rows, columns, getRowId: (b) => b.ref, messages: EN });A merged column’s value is its fields’ non-empty values joined by « – »; give it your own value, codec or a cell
part (ui.cell, which receives the whole row) for anything else. With server data, the columns come from the first
answer and then stay: see Server data.
Totals row
Section titled “Totals row”total on a column — 'sum', 'avg', 'min', 'max', 'count', 'first', 'last', or your function of the
column’s values — and the totalsRow() feature from @dzgrid/dom/totals-row give a « Total » row at the bottom of the
grid, always in view, over the rows that pass the filters (every page), edits included. It is a row of the pinned
bottom band, so pinnedRows() is needed too (texts -pinned and -totals, CSS pinned-rows.css). The CSV and Excel
files and the printed table end with it; a JSON file does not (it holds records only).
import { createGrid, type GridMessages } from '@dzgrid/dom';import { pinnedRows } from '@dzgrid/dom/pinned-rows';import { totalsRow } from '@dzgrid/dom/totals-row';import { EN } from '@dzgrid/dom/locales/en';import { EN_PINNED } from '@dzgrid/dom/locales/en-pinned';import { EN_TOTALS } from '@dzgrid/dom/locales/en-totals';import '@dzgrid/theme/pinned-rows.css';
type Harvest = { id: string; orchard: string; crates: number; sugar: number };const totals = totalsRow();const messages: GridMessages = { ...EN, text: { ...EN.text, ...EN_PINNED.text, ...EN_TOTALS.text } };
const grid = createGrid<Harvest>(document.getElementById('harvest')!, { rows: [{ id: 'h1', orchard: 'North slope', crates: 41, sugar: 14.2 }, { id: 'h2', orchard: 'Old mill', crates: 17, sugar: 12.8 }], columns: [ { id: 'orchard', header: 'Orchard' }, { id: 'crates', header: 'Crates', type: 'number', total: 'sum' }, { id: 'sugar', header: 'Sugar (°Bx)', type: 'number', total: 'avg', numberFormat: { maximumFractionDigits: 1 } }, ], getRowId: (h) => h.id, messages, features: [pinnedRows<Harvest>(), totals],});
console.log(totals.api(grid)?.values()); // { crates: 58, sugar: 13.5 }The same rules as the group totals (sums without drift, means over the numbers present, count counting every row).
A total that does not fit its column (a sum of text) is told once as a problem. With server pages the grid holds one
page, so it computes nothing: give the totals your server sends with api.set({ crates: 1234 }).
Vertical titles
Section titled “Vertical titles”headerVertical: true on a column, with the verticalHeaders() feature from @dzgrid/dom/vertical-headers (CSS
vertical-headers.css), writes its title upwards, so a grid of many narrow columns keeps them narrow. The header row
grows to the longest such title (at most maxHeight, 240 px by default). Keys and screen readers are unchanged.
import { createGrid } from '@dzgrid/dom';import { verticalHeaders } from '@dzgrid/dom/vertical-headers';import { EN } from '@dzgrid/dom/locales/en';import '@dzgrid/theme/vertical-headers.css';
type Plot = { id: string; bed: string; mon: boolean; tue: boolean; wed: boolean };const day = (id: 'mon' | 'tue' | 'wed', header: string) => ({ id, header, type: 'boolean' as const, width: 44, headerVertical: true });
createGrid<Plot>(document.getElementById('watering')!, { rows: [{ id: 'p1', bed: 'Herb spiral', mon: true, tue: false, wed: true }], columns: [{ id: 'bed', header: 'Bed', flex: 1, minWidth: 140 }, day('mon', 'Monday'), day('tue', 'Tuesday'), day('wed', 'Wednesday')], getRowId: (p) => p.id, messages: EN, features: [verticalHeaders({ maxHeight: 120 })],});Titles on several lines
Section titled “Titles on several lines”headerWrap: true on a column, with the wrappedHeaders() feature from @dzgrid/dom/wrapped-headers (no stylesheet),
puts a long title on several lines (at most maxLines, 3 by default, then « … ») and grows the column headers’ row to
the tallest — measured again when a column is resized or the text size changes, never while scrolling. With
headerHeight the taller wins. Fitting such a column to its content takes its cells and the title’s longest word. A
title cut at the cap sits at the start of its cell, even in a number column, so its « … » shows in every browser.
import { createGrid } from '@dzgrid/dom';import { wrappedHeaders } from '@dzgrid/dom/wrapped-headers';import { EN } from '@dzgrid/dom/locales/en';
type Parcel = { id: string; route: string; days: number };
createGrid<Parcel>(document.getElementById('parcels')!, { rows: [{ id: 'p1', route: 'Harbour – Hill farm', days: 3 }], columns: [{ id: 'route', header: 'Route', flex: 1 }, { id: 'days', header: 'Average delivery time in days', type: 'number', width: 90, headerWrap: true }], getRowId: (p) => p.id, messages: EN, features: [wrappedHeaders({ maxLines: 2 })],});No header row on screen
Section titled “No header row on screen”The grid setting headers: 'hidden' (live; Angular [headers], Web Component headers="hidden") shows no header row.
The headers stay in the page for screen readers, and appear while the keyboard is in them (ArrowUp from the first
row), so sorting and the column menu still work from the keyboard. With the mouse, sort and filter through the cell
menu, the dock’s panels or your own controls. Print and the files keep their header row unless you pass
header: false.
Columns that collapse on narrow screens
Section titled “Columns that collapse on narrow screens”With the responsive() feature from @dzgrid/dom/responsive, columns that do not fit the view are taken out of the
rows instead of making the grid scroll sideways: a chevron on each row’s first cell opens « Title: value » lines under
it (the detail rows’ panel, so the -detail texts are needed). The last columns go first; collapsePriority on a
column (higher goes first, 0 never) or responsive({ priority: { id: n } }) change the order. The first column and
pinned columns never collapse. A flexible column counts its minWidth. It follows every resize; the quick search, the
filters, the files and print still see every column, and nothing is saved in the grid state.
import { createGrid, type GridMessages } from '@dzgrid/dom';import { responsive } from '@dzgrid/dom/responsive';import { EN } from '@dzgrid/dom/locales/en';import { EN_DETAIL } from '@dzgrid/dom/locales/en-detail';
type Hive = { id: string; name: string; apiary: string; frames: number; queen: string; checked: string };const messages: GridMessages = { ...EN, text: { ...EN.text, ...EN_DETAIL.text } };
createGrid<Hive>(document.getElementById('hives')!, { rows: [{ id: 'h1', name: 'Clover', apiary: 'Hill', frames: 9, queen: 'Marked blue', checked: '2026-05-02' }], columns: [ { id: 'name', header: 'Hive', width: 160 }, { id: 'apiary', header: 'Apiary', width: 140, collapsePriority: 2 }, { id: 'frames', header: 'Frames', type: 'number', width: 100, collapsePriority: 0 }, { id: 'queen', header: 'Queen', width: 180 }, { id: 'checked', header: 'Checked', type: 'day', width: 140 }, ], getRowId: (h) => h.id, messages, features: [responsive()],});On a row’s first cell, Enter or Space opens and closes its lines (arrows too, as for detail rows). It is a detail rows
feature: it cannot be combined with a tree, row grouping, header groups, merged cells (spans()) or your own
detail(): the grid tells unsupported-combination and nothing collapses.
Column groups
Section titled “Column groups”A group has children and an optional header; collapsible groups fold to the columns marked
groupShow: 'closed'. Group header rows need the spans() feature; the groups() feature from @dzgrid/dom/groups
adds the button that opens and closes a collapsible group (its texts are in the -columns bundle).
import { createGrid } from '@dzgrid/dom';import type { ColumnOrGroup } from '@dzgrid/core';import { spans } from '@dzgrid/dom/spans';import { groups } from '@dzgrid/dom/groups';import { EN } from '@dzgrid/dom/locales/en';import { EN_COLUMNS } from '@dzgrid/dom/locales/en-columns';
type Station = { id: string; name: string; tempMin: number; tempMax: number; rain: number };
const columns: ColumnOrGroup<Station>[] = [ { id: 'name', header: 'Station' }, { id: 'weather', header: 'Weather', collapsible: true, children: [ { id: 'tempMin', header: 'Min °C', type: 'number' }, { id: 'tempMax', header: 'Max °C', type: 'number', groupShow: 'closed' }, { id: 'rain', header: 'Rain (mm)', type: 'number' }, ] },];
createGrid<Station>(document.getElementById('stations')!, { rows: [{ id: 's1', name: 'Cape Lumen', tempMin: 4, tempMax: 17, rain: 2.5 }], columns, getRowId: (s) => s.id, messages: { ...EN, text: { ...EN.text, ...EN_COLUMNS.text } }, features: [spans(), groups()],});import { Component } from '@angular/core';import { DzGrid, type DzgColumnOrGroup } from '@dzgrid/angular';import { spans } from '@dzgrid/dom/spans';import { groups } from '@dzgrid/dom/groups';import { EN } from '@dzgrid/dom/locales/en';import { EN_COLUMNS } from '@dzgrid/dom/locales/en-columns';
type Station = { id: string; name: string; tempMin: number; tempMax: number; rain: number };
@Component({ selector: 'app-stations', imports: [DzGrid], template: `<dz-grid [rows]="rows" [columns]="columns" [getRowId]="rowId" [messages]="messages" [features]="features" />`,})export class Stations { readonly rows: Station[] = [{ id: 's1', name: 'Cape Lumen', tempMin: 4, tempMax: 17, rain: 2.5 }]; readonly columns: DzgColumnOrGroup<Station>[] = [ { id: 'name', header: 'Station' }, { id: 'weather', header: 'Weather', collapsible: true, children: [ { id: 'tempMin', header: 'Min °C', type: 'number' }, { id: 'tempMax', header: 'Max °C', type: 'number', groupShow: 'closed' }, { id: 'rain', header: 'Rain (mm)', type: 'number' }, ] }, ]; readonly rowId = (s: Station) => s.id; readonly messages = { ...EN, text: { ...EN.text, ...EN_COLUMNS.text } }; readonly features = [spans(), groups()];}import '@dzgrid/element/define';import type { DzGridElement } from '@dzgrid/element';import { spans } from '@dzgrid/dom/spans';import { groups } from '@dzgrid/dom/groups';import { EN } from '@dzgrid/dom/locales/en';import { EN_COLUMNS } from '@dzgrid/dom/locales/en-columns';
type Station = { id: string; name: string; tempMin: number; tempMax: number; rain: number };
const grid = document.querySelector<DzGridElement<Station>>('dz-grid')!;grid.columns = [ { id: 'name', header: 'Station' }, { id: 'weather', header: 'Weather', collapsible: true, children: [ { id: 'tempMin', header: 'Min °C', type: 'number' }, { id: 'tempMax', header: 'Max °C', type: 'number', groupShow: 'closed' }, { id: 'rain', header: 'Rain (mm)', type: 'number' }, ] },];grid.getRowId = (s) => s.id;grid.messages = { ...EN, text: { ...EN.text, ...EN_COLUMNS.text } };grid.features = [spans(), groups()];grid.rows = [{ id: 's1', name: 'Cape Lumen', tempMin: 4, tempMax: 17, rain: 2.5 }];Drawn by the Web Component. The group's button folds it to its Max °C column.
spans() also merges cells: merge: true on a column joins equal neighbours, rowSpan(ctx) and colSpan(ctx)
decide from the data, and the fullWidth setting draws a row across every column.
Classes, tooltips, custom cells
Section titled “Classes, tooltips, custom cells”ui.cellClass(c)returns a class for one cell;ui.headerClassa class for the header; the grid settingrowClass(c)a class for a row.tooltip(value, row, context)returns the text shown on hover and focus;headerTooltipthe header’s.ui.cell,ui.header,ui.tooltipname a part (custom drawing, see below).
import { createGrid, definePart } from '@dzgrid/dom';import { EN } from '@dzgrid/dom/locales/en';import '@dzgrid/theme/dzgrid.css';
type Bee = { id: string; hive: string; honey: number };
const jar = definePart('cell', { mount(host, c) { const span = host.ownerDocument.createElement('span'); host.append(span); const draw = (n: typeof c) => { span.textContent = n.value == null ? '' : `${n.text} kg`; }; draw(c); return { update: draw, destroy: () => span.remove() }; },});
createGrid<Bee>(document.getElementById('hives')!, { rows: [{ id: 'h1', hive: 'Clover hill', honey: 12.5 }], columns: [ { id: 'hive', header: 'Hive', tooltip: (v) => (v ? `Hive ${v}` : null) }, { id: 'honey', header: 'Honey', type: 'number', ui: { cell: 'jar', cellClass: (c) => (Number(c.value) > 10 ? 'is-full' : null) } }, ], getRowId: (b) => b.id, messages: EN, parts: { jar }, rowClass: (c) => (c.row.honey === 0 ? 'is-empty' : null),});The grid never writes HTML strings: a part builds DOM in the host it is given, and the grid keeps the cell’s role,
focus and keyboard. Header, tooltip and overlay parts need the slots() feature from @dzgrid/dom/slots. Ready-made
cells (badge, cellBoolean) are in @dzgrid/dom/parts. In Angular a part is a component: give the class itself in
ui.cell (see Getting started with Angular).
Moving, resizing, hiding
Section titled “Moving, resizing, hiding”columns()from@dzgrid/dom/columns: drag a header to move it, drag its edge to resize, double click the edge to fit the content; the keyboard does the same (texts-columns, CSScolumns.css).columnMenu()from@dzgrid/dom/column-menu: the menu on each header (sort, pin, size, hide, group…; texts-column-menu, CSSmenu.css). Give it the other features it can drive inuse.columnsChooser()from@dzgrid/dom/columns-panel: a panel to show, hide and order columns (see Dock and panels).
import { createGrid, type GridMessages } from '@dzgrid/dom';import { columns } from '@dzgrid/dom/columns';import { columnMenu } from '@dzgrid/dom/column-menu';import { columnsChooser } from '@dzgrid/dom/columns-panel';import { EN } from '@dzgrid/dom/locales/en';import { EN_COLUMNS } from '@dzgrid/dom/locales/en-columns';import { EN_COLUMN_MENU } from '@dzgrid/dom/locales/en-column-menu';import '@dzgrid/theme/dzgrid.css';import '@dzgrid/theme/columns.css';import '@dzgrid/theme/menu.css';import '@dzgrid/theme/columns-panel.css';
type Shell = { id: string; name: string; beach: string; size: number };
const moves = columns();const chooser = columnsChooser();const messages: GridMessages = { ...EN, text: { ...EN.text, ...EN_COLUMNS.text, ...EN_COLUMN_MENU.text } };
const grid = createGrid<Shell>(document.getElementById('shells')!, { rows: [{ id: 'x1', name: 'Spiral cone', beach: 'Amber bay', size: 4.2 }], columns: [{ id: 'name', header: 'Shell' }, { id: 'beach', header: 'Beach' }, { id: 'size', header: 'Size (cm)', type: 'number' }], getRowId: (s) => s.id, messages, features: [moves, chooser, columnMenu({ use: { columns: moves, chooser } })],});
moves.api(grid)?.autoSizeAll();grid.kernel.dispatch({ type: 'column.state', patch: [{ id: 'beach', hidden: true }] }, 'api');Every feature that has an api gives it through feature.api(grid) (in Angular, ctrl.use(feature)); it is null
until the grid exists.