Rendering
Rows are virtualized automatically; on top of that, cells accept custom renderers — plain functions, classes, or framework components — and rows accept semantic presentation, conditional classes, and full-width takeovers.
- React
- Angular
- Core TypeScript
function StatusBadge({ value, valueFormatted, isGroup }: CellRendererParams) {
if (isGroup) return valueFormatted ?? null; // a group row: its aggregate as text, or nothing
return <span className={"status status-" + String(value).toLowerCase()}>{String(value)}</span>;
}
const columns: ReactColDef[] = [
{ key: "status", label: "Status", cellRenderer: StatusBadge },
{
colId: "trend",
label: "Trend",
valueGetter: (row) => row.monthlyRevenue,
cellRenderer: SparklineRenderer,
cellRendererParams: { type: "area", showPoints: true },
},
];
<Grid
rowData={rows}
columnDefs={columns}
zebraRows
columnHover
/>
// Any Angular component works in a cell slot; params arrive as an input.
columns: NgColDef[] = [
{ key: "status", label: "Status", cellRenderer: StatusBadgeComponent },
{
colId: "trend",
label: "Trend",
valueGetter: (row: Order) => row.monthlyRevenue,
cellRenderer: SparklineRenderer,
cellRendererParams: { type: "area", showPoints: true },
},
];
<awb-grid
[rowData]="rows"
[columnDefs]="columns"
[zebraRows]="true"
[columnHover]="true"
/>
const core = new GridCore(measurer, {
columnDefs: [
{
key: "status",
label: "Status",
cellRenderer: ({ value, valueFormatted, isGroup }) => {
if (isGroup) return valueFormatted; // a group row: its aggregate as text, or nothing
const badge = document.createElement("span");
badge.className = "status status-" + String(value).toLowerCase();
badge.textContent = String(value);
return badge;
},
},
{
colId: "trend",
label: "Trend",
valueGetter: (row) => row.monthlyRevenue,
cellRenderer: SparklineRenderer,
cellRendererParams: { type: "area", showPoints: true },
},
],
zebraRows: true,
columnHover: true,
});
Virtual scrolling and overscan
const options = {
rowHeight: 40,
overscanRowCount: 8,
} satisfies GridOptions;
rowHeight is fixed when the grid is created. Without it the theme's
rowHeight param applies (see Theming), and otherwise 43px.
Virtualization is automatic: the renderer maintains a small reusable row pool and slides it over the current viewport. Left, center, and right column sections stay synchronized without additional configuration.
Function cell renderer
const column = {
key: "status",
label: "Status",
cellRenderer: ({ value, valueFormatted, isGroup }) => {
if (isGroup) return valueFormatted; // a group row: the aggregate as text, or nothing
const badge = document.createElement("span");
badge.className = `status status-${String(value).toLowerCase()}`;
badge.textContent = String(value);
return badge;
},
} satisfies ColDef;
Class renderers implement init, getGui, and optional refresh/destroy.
The React and Angular bindings additionally accept framework components in the
same cellRenderer slot, as the tabs above show.
Built-in renderers
The grid ships renderers for a change flash, sparklines, a boolean checkbox, links,
images, progress bars and star ratings. The last four are also reachable by name
(cellRenderer: "link" | "image" | "progress" | "rating") and read their options
from cellRendererParams; each is exported as a class (LinkCellRenderer, …)
with a params type (LinkCellRendererParams, …). Like every renderer they change
only what the cell shows: export, the clipboard, tooltips and quick-filter
matching still read the formatted value, and group rows show the group cell value
as text.
const priceColumn = {
key: "price",
label: "Price",
type: ColumnType.CURRENCY,
cellRenderer: ChangeFlashCellRenderer,
cellRendererParams: { cellFlashDuration: 400, cellFadeDuration: 900 },
} satisfies ColDef;
const trendColumn = {
colId: "trend",
label: "Trend",
valueGetter: (row) => row.monthlyRevenue,
cellRenderer: SparklineRenderer,
cellRendererParams: {
type: "area",
showPoints: true,
tooltipValueFormatter: ({ xValue, yValue }) => `${xValue}: $${yValue}`,
},
} satisfies ColDef;
Grid-wide flash defaults are available as cellFlashDuration and
cellFadeDuration. Sparkline data may be number[] or [xValue, number][];
supported visuals are line, area, and bar. The
Sparklines page covers series shapes, tooltips,
theming, and the built-in column generator in full.
Boolean checkbox
A ColumnType.BOOLEAN column that sets no cellRenderer of its own draws a
checkbox (BooleanCellRenderer) instead of the text "true" or "false". true is
a ticked box, false an empty one, and a blank value (null or undefined) the
same indeterminate dash as the header's select-all checkbox.
const activeColumn = {
key: "active",
label: "Active",
type: ColumnType.BOOLEAN,
editable: true,
valueFormatter: ({ value }) => (value == null ? "" : value ? "Yes" : "No"),
} satisfies ColDef;
- Read-only by default. When the cell is not editable the box is shown but
inert, dimmed and marked
aria-readonly. - Click to toggle. When the cell is editable — the column's
editable, a row presentation'seditable,pinnedRowsEditableon pinned rows, and aneditTriggerother than"none"— a plain click on the box writes the opposite value throughapi.setCellValue.onBeforeCellCommit,cellValueChanged, undo/redo and no-op suppression all apply; a blank value toggles totrue. Clicking the box never opens the editor, not even witheditTrigger: "singleClick", and a double-click on it is two toggles. Shift/Ctrl/Cmd-clicks are left to selection, and a click elsewhere in the cell focuses, selects and (pereditTrigger) edits it as usual. - Keyboard. There is no extra key binding: Enter or F2 opens the built-in
"boolean"editor as before. - Accessibility. The box has
role="checkbox"andaria-checked("mixed"for a blank value), and the cell keeps itsgridcellrole. Itsaria-labelis the formatted value when avalueFormatterputs it into words ("Yes"/"No"), otherwise the column's label. - Text everywhere else. Export, the clipboard and configured tooltips still use the formatted value; only the cell shows the box. Group rows show the group cell value as text. The quick-filter find skips these columns, since their cells show no text to highlight.
To keep a boolean column as text, set cellRenderer: "text". Setting any other
renderer (for example ChangeFlashCellRenderer) replaces the checkbox, and
cellRenderer: BooleanCellRenderer draws the box on a column of another type.
Link
cellRenderer: "link" draws an <a> to the cell value, with the formatted value
as its text. It opens in a new tab by default, with rel="noopener noreferrer".
const siteColumn = {
key: "site",
label: "Website",
cellRenderer: "link",
cellRendererParams: {
href: ({ value }) => `https://${value}`,
text: ({ node }) => node.data.company,
},
} satisfies ColDef;
| Param | Default | Effect |
|---|---|---|
href(params) | the value | The URL. A blank result shows the text without a link |
text(params) | the formatted value | The link text |
target | "_blank" | Where the link opens; "_blank" adds noopener noreferrer |
rel | — | Extra rel tokens |
download | — | true, or a file name, sets the download attribute |
A blank value renders nothing, and a javascript: or vbscript: URL is never
linked (the text shows plain). A click on the link navigates and still selects the
cell, but never opens the editor — neither editTrigger: "singleClick" nor a
double-click on the link. Middle-, Ctrl- and Cmd-clicks behave as the browser's.
The link takes no tab stop of its own, so keyboard users keep the grid's cell
navigation.
Image
cellRenderer: "image" draws an <img loading="lazy"> of the cell value, scaled
to fit the row (object-fit: contain).
const avatarColumn = {
key: "avatarUrl",
label: "Avatar",
width: 72,
cellRenderer: "image",
cellRendererParams: {
alt: ({ node }) => `${node.data.name}'s avatar`,
height: 28,
},
} satisfies ColDef;
| Param | Default | Effect |
|---|---|---|
src(params) | the value | The image URL. A blank result renders nothing |
alt(params) | formatted value / column label | Alternative text, also shown when the image fails to load |
width, height | fit the cell | Size in pixels (a number) or any CSS length |
Without alt, the alternative text is the formatted value when a
valueFormatter turns the URL into words, otherwise the column's label. A blank
value renders nothing, and a broken image shows its alt text instead of the
browser's broken-image glyph. The quick-filter find skips image columns, since
their cells show no text to highlight.
Progress
cellRenderer: "progress" draws a track with a bar filled to the value. The value
is a fraction from 0 to 1, or value / max when max is given.
const doneColumn = {
key: "percentDone",
label: "Done",
type: ColumnType.NUMBER,
valueFormatter: ({ value }) => (value == null ? "" : `${value}%`),
cellRenderer: "progress",
cellRendererParams: {
max: 100,
showValue: true,
color: ({ fraction }) =>
fraction == null ? undefined : fraction < 0.3 ? "#ef4444" : fraction < 0.7 ? "#f59e0b" : "#22c55e",
},
} satisfies ColDef;
| Param | Default | Effect |
|---|---|---|
max | 1 (a fraction) | The value that fills the bar |
showValue | false | Print the formatted value beside the bar |
color(params) | theme colour | The bar's colour for this cell; params.fraction is the clamped fill (0–1) or null |
className | — | Extra class(es) on the renderer root, a string or (params) => string |
Values outside the range clamp, and a blank or non-numeric value shows an empty
track. The bar has role="progressbar" with aria-valuemin 0, aria-valuemax
(max, else 1), aria-valuenow, aria-valuetext (the formatted value) and the
column label as its aria-label. The theme parameters progressTrackColor and
progressBarColor (--pte-progress-track-color, --pte-progress-bar-color) set
the default colours. Without showValue the quick-filter find skips the column.
Rating
cellRenderer: "rating" draws max stars (5 by default) filled to the value, with
a half star for a fraction.
const ratingColumn = {
key: "rating",
label: "Rating",
type: ColumnType.NUMBER,
editable: true,
cellRenderer: "rating",
cellRendererParams: { max: 5, allowClear: true },
} satisfies ColDef;
| Param | Default | Effect |
|---|---|---|
max | 5 | Number of stars |
allowClear | false | Clicking the current star clears the rating to 0 |
- Read-only by default. The stars are one
role="img"element labelled "3 of 5"; a blank value shows empty stars. - Click to rate. When the cell is editable (the same rule as the boolean
checkbox), a plain click on star N writes N through
api.setCellValue, soonBeforeCellCommit,cellValueChanged, undo/redo and no-op suppression apply. Clicking the current star writes nothing unlessallowClearis set. The click never opens the editor, not even witheditTrigger: "singleClick", and modified clicks are left to selection. - Keyboard. Enter or F2 opens the column's editor as usual (the number
editor on a
ColumnType.NUMBERcolumn).
The theme parameters ratingColor and ratingEmptyColor (--pte-rating-color,
--pte-rating-empty-color) colour the stars, and linkColor (--pte-link-color)
the link renderer's text. The quick-filter find skips rating columns.
Group rows
When rows are grouped with groupDisplayType: "singleColumn" or
"multipleColumns", a column's cellRenderer runs on group rows as well as on
leaves. A group row has no data of its own, so its params describe the group:
isGroupistrueon a group row andfalseon a leaf;nodeis the row node either way.valueis the group cell value: the column's aggregate when one is assigned, else itsgroupValueGetterresult, elseundefined.valueFormattedis that value through the column'svalueFormatter(an empty string when there is no value).datais the group node. Its owndatais a placeholder on the client-side row model and the raw group row the data source returned on the server-side one.
The label columns — the auto-group column, the tree column, and every grouped
column of multipleColumns display — never run a renderer on a group row. They
keep the chevron, indent, and label the row depends on, which a renderer written
for the leaf value could not be expected to reproduce; on leaves they run their
renderer as usual. In groupDisplayType: "groupRows" a group row is one
full-width cell painted by fullWidthCellRenderer (whose params carry
isGroup and node too), so per-column renderers do not run there.
A renderer instance is pooled per cell slot and follows one lifecycle across
leaf and group rows: as a slot scrolls from a leaf to a group, the same
instance's refresh receives the group params. Return false from refresh to
have the grid destroy it and build a new one instead.
A renderer written for leaves should check isGroup first. Returning
valueFormatted shows exactly what the cell showed before renderers ran on
group rows — the aggregate as text, or nothing:
const column = {
key: "status",
label: "Status",
cellRenderer: (params) => {
if (params.isGroup) return params.valueFormatted; // the aggregate, or nothing
return renderStatusBadge(params.value);
},
} satisfies ColDef;
A renderer that should paint group rows too — a bar, a coloured total — reads
value there like on a leaf. The
grouping page covers the group cell
value rule and groupValueGetter.
Row presentation
For a semantic state shared by every cell in a row, supply one row presentation instead of repeating cell callbacks across columns:
const pendingIds = new Set(["order-42"]);
const options = {
getRowPresentation: ({ rowId }) => pendingIds.has(rowId) ? {
rowClass: "row-pending",
cellClass: "cell-pending",
cellStyle: { opacity: "0.7" },
editable: false,
tooltip: {
content: "Saving changes",
options: { mode: "follow" },
},
accessibility: {
description: "Changes to this row are being saved",
busy: true,
},
metadata: { status: "pending" },
} : undefined,
} satisfies GridOptions;
// Required only when the captured Set changes without a row transaction.
api.refreshRowPresentation();
Explicit column tooltip content/options override the row defaults. Cell classes
compose; cell inline styles merge with the column winning conflicting fields.
editable: false prevents user editing in columns that inherit the row gate; it
does not enable columns when true. A deliberate exception can set
inheritRowPresentation: { editable: false }.
Full-width rows
const options = {
isFullWidthRow: (node) => node.data?.kind === "notice",
fullWidthCellRenderer: ({ data }) => {
const element = document.createElement("strong");
element.textContent = data.message;
return element;
},
} satisfies GridOptions;
Full-width rows are client-side and retain the standard row height.
Conditional rows and visual states
const options = {
rowHover: true,
columnHover: true,
zebraRows: true,
getRowClass: ({ data }) => data.overdue ? "row-overdue" : undefined,
getRowStyle: ({ data }) => ({ opacity: data.archived ? "0.6" : "1" }),
} satisfies GridOptions;
Loading and empty overlays
<Grid
loading={isLoading}
loadingMessage="Loading orders…"
noRowsMessage="No orders match this view"
/>
For the framework-neutral API:
api.showLoadingOverlay();
api.hideOverlay();
api.showNoRowsOverlay(); // force the empty state, e.g. after a failed load
The no-rows overlay appears automatically for an empty model and distinguishes
an empty data set from a filtered-out result. showNoRowsOverlay() shows it over
rows too, until the next data change re-decides it; hideOverlay() leaves it up
on a grid that really is empty.
Both messages are live: api.updateGridOptions({ loadingMessage, noRowsMessage })
relabels the overlays on a mounted grid (passing undefined restores the
default).