Conditional Formatting
Colour cells and rows from their values — numeric thresholds, ranges, text matches, status icons and row tints — without writing a single cell template.
column.conditionalFormats
Declarative rules per column. Day % crosses two thresholds, P/E highlights a band, Shares shows what happens when two rules match one cell, and Signal uses a predicate no operator could express.
One rule per condition, in the order you write them
Each rule tests the cell's value. A match applies an inline style, a
className, an icon before the value — or all three. Day %
above has two rules that cannot both match, which is the simple case: below zero is red with a ▼,
at or above +2% is green with a ▲, and everything between is left alone.
conditionalFormats: [
{ op: 'lt', value: 0, style: { color: '#dc2626' }, icon: '▼' },
{ op: 'gte', value: 2, style: { color: '#16a34a' }, icon: '▲' },
]
When two rules match one cell
Rules run top to bottom and the matches merge — they do not compete. The Shares column has three rules over one number, and the two big positions show what that means:
- 5000 matches the first two rules. The colour is the second rule's — a later rule wins a style key it also sets. So does the icon: the last matched icon is the one you see, 🔥 rather than ⚠.
- That second rule carries
stopIfMatched: true, so the third rule never runs and the cell is not underlined. - 2400 matches only the first rule, then the third: amber, ⚠, and underlined. Same three rules, a different outcome, decided entirely by the value.
classNames behave differently from styles: every matched rule's class is
added, so nothing overrides anything.
The operators, and the escape hatch
eq · neq · gt · gte · lt ·
lte · between · in · contains ·
startsWith · endsWith · empty · notEmpty.
betweenusesvalueandvalue2and is inclusive at both ends — the P/E band above is 12 to 25, and a P/E of exactly 12 is inside it.intakes an array: the Sector column italicises Energy and Consumer with a single rule.
When no operator fits, pass when instead. It receives the value and the whole
row, so it can compare two fields — which is the one thing the operators cannot do. The
Signal column marks a holding that is up on the day while the analyst says Sell,
and only one row on the page qualifies:
{ when: (value, row) => row.changePct > 0 && value === 'Sell',
style: { color: '#dc2626', fontWeight: '700' }, icon: '↯' }
when takes precedence over op: if you set both, the operator is ignored.
column.cellStyle
One style for every cell in a column, or a function per row. Use it for alignment and number formatting, and for rules the operators cannot express.
Two forms, one property
cellStyle is for styling that is not a condition. Give it a flat map to style
every cell in the column — Price above is right-aligned with tabular numerals, so
the digits line up as the rows scroll. Give it a function (row, value) => CellStyle
for per-row logic, and return an empty object to mean "leave this cell alone".
// static
cellStyle: { fontVariantNumeric: 'tabular-nums', textAlign: 'right' },
// per row
cellStyle: (row, value) => value >= 1000 ? { fontWeight: '700' } : {},
When to reach for this instead of a rule
A rule is a better home for anything a reader would describe as a threshold, because the intent is
readable at a glance and it survives being reordered. cellStyle earns its place for
column-wide presentation, and for logic with no natural operator.
cellClass is the same property with a class instead of a style, in both forms — use it
when the styling belongs in your stylesheet. The two combine: a class from
cellClass and a class from a matched rule both end up on the cell. The
Analyst column above marks Sell with a class, and the colour comes from CSS.
Two things to get right when you do that. The cell is rendered by the grid, so a component-scoped
selector will not reach it — reach through with ::ng-deep (or put the rule in a global
stylesheet). And make the selector specific enough to win: a bare
.is-sell ties with the grid's own .gc-cell rule and loses, because the
library's stylesheet is loaded later. Include the cell:
:host ::ng-deep .gc-cell.is-sell {
color: #dc2626;
font-weight: 700;
}
Do not put layout properties in here
width, flex, min-width and max-width belong to
the column, not to a style map. A cellStyle lands on the very same element the grid
sizes, so a width set here overrides the column's own width and breaks the layout —
it is not filtered out for you. Set sizing through width / sizing /
minWidth on the column instead.
config.rowStyle
Styles the whole row. Here every Sell-rated holding is tinted red and every other row is striped — one function, both jobs.
One function, the whole row
config.rowStyle takes a flat map, or a function of
(row, index). The index is what makes striping possible, and the row is what makes a
status tint possible — the grid above does both in one expression, and the status tint wins where
the two overlap:
rowStyle: (row, index) =>
row.rating === 'Sell' ? { background: 'rgba(220,38,38,.08)' }
: index % 2 === 1 ? { background: 'rgba(100,116,139,.06)' }
: {},
Sort by Analyst and watch what happens: the red rows gather together and the
stripes re-lay themselves around them, because index is the row's position
now, not its position in your data.
Row and cell styling stack — a row background shows through wherever a cell sets none of its own.
Value-driven cell & row styling — no templates required
Conditional formatting paints cells and rows straight from their data, so you do not have to write
a cellTemplate just to change a colour. Everything on this page is in the free
package. There are three entry points, and they are the three examples below:
column.conditionalFormats— a list of declarative rules. Reach for this first.column.cellStyle— one style for the whole column, or a function per row.config.rowStyle— the whole row.
They compose. A cell can take a column-wide cellStyle, have a matched rule override
part of it, and sit on a row tinted by rowStyle — all at once. Where a cell rule and
a cellStyle set the same property, the rule wins.
Formatting API
| Property | Type | Default | Description |
|---|---|---|---|
| column.conditionalFormats | ConditionalFormatRule[] | — | Declarative rules, evaluated top → bottom. Matches merge: a later rule wins a style key it also sets, classNames accumulate, and the last matched icon is shown. |
| rule.op | 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'between' | 'in' | 'contains' | 'startsWith' | 'endsWith' | 'empty' | 'notEmpty' | — | The value test. between uses value + value2 and is inclusive at both ends; in takes an array. |
| rule.when | (value, row) => boolean | — | Predicate form — sees the whole row, so it can compare fields. Takes precedence over op. |
| rule.style | CellStyle | — | Inline styles applied when the rule matches. Merged with earlier matches, key by key. |
| rule.className | string | — | A class added when the rule matches. Every matched rule contributes; none overrides another. |
| rule.icon | string | — | A glyph shown before the value. When several matched rules set one, the last wins. |
| rule.stopIfMatched | boolean | false | Halt evaluation after this rule matches, so no later rule can add to the cell. |
| column.cellStyle | CellStyle | (row, value) => CellStyle | — | Static or per-row styling for every cell in the column. A matched rule overrides it key by key. |
| column.cellClass | string | (row, value) => string | Record<string, boolean> | — | The same two forms with a class instead of a style. Combines with rule classNames. |
| config.rowStyle | CellStyle | (row, index) => CellStyle | — | Styles the whole row. index is the row's current display position, so striping follows the sort. |