Skip to main content

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.

Loading interactive example…
React
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
/>

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's editable, pinnedRowsEditable on pinned rows, and an editTrigger other than "none" — a plain click on the box writes the opposite value through api.setCellValue. onBeforeCellCommit, cellValueChanged, undo/redo and no-op suppression all apply; a blank value toggles to true. Clicking the box never opens the editor, not even with editTrigger: "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 (per editTrigger) 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" and aria-checked ("mixed" for a blank value), and the cell keeps its gridcell role. Its aria-label is the formatted value when a valueFormatter puts 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.

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;
ParamDefaultEffect
href(params)the valueThe URL. A blank result shows the text without a link
text(params)the formatted valueThe 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;
ParamDefaultEffect
src(params)the valueThe image URL. A blank result renders nothing
alt(params)formatted value / column labelAlternative text, also shown when the image fails to load
width, heightfit the cellSize 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;
ParamDefaultEffect
max1 (a fraction)The value that fills the bar
showValuefalsePrint the formatted value beside the bar
color(params)theme colourThe 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;
ParamDefaultEffect
max5Number of stars
allowClearfalseClicking 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, so onBeforeCellCommit, cellValueChanged, undo/redo and no-op suppression apply. Clicking the current star writes nothing unless allowClear is set. The click never opens the editor, not even with editTrigger: "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.NUMBER column).

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:

  • isGroup is true on a group row and false on a leaf; node is the row node either way.
  • value is the group cell value: the column's aggregate when one is assigned, else its groupValueGetter result, else undefined. valueFormatted is that value through the column's valueFormatter (an empty string when there is no value).
  • data is the group node. Its own data is 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).