Conditional formatting

Conditional formatting colors a cell by its value. SvGrid ships it as a declarative engine prop, conditionalFormats, so you describe the rules once and the grid paints every cell - no per-cell cell snippet required.

Open the live example: Conditional formatting (engine) (Rows & Cells)

It goes beyond the cellClass(ctx) callback (which only toggles static CSS classes): color scales and data bars need a value computed against the column's min/max range, which the engine does for you.

The examples on this page run against these rows:

<script lang="ts">
  import { SvGrid, type GridColumns } from '@svgrid/grid'

  type Person = {
    id: number
    name: string
    email: string
    department: string
    age: number
    salary: number
    city: string
    startDate: string
    active: boolean
  }

  const people: Person[] = [
    { id: 1, name: 'Ada Lovelace',   email: '[email protected]',   department: 'Engineering', age: 36, salary: 142000, city: 'London',   startDate: '2021-03-01', active: true },
    { id: 2, name: 'Grace Hopper',   email: '[email protected]', department: 'Engineering', age: 45, salary: 168000, city: 'New York', startDate: '2019-07-15', active: true },
    { id: 3, name: 'Linus Torvalds', email: '[email protected]', department: 'Platform',    age: 54, salary: 155000, city: 'Portland', startDate: '2020-01-20', active: false },
    { id: 4, name: 'Radia Perlman',  email: '[email protected]', department: 'Networking',  age: 49, salary: 161000, city: 'Seattle',  startDate: '2022-09-05', active: true },
    { id: 5, name: 'Barbara Liskov', email: '[email protected]', department: 'Platform',  age: 52, salary: 172000, city: 'Boston',   startDate: '2018-11-11', active: true },
  ]

  const data = people

  const columns: GridColumns<Person> = [
    { field: 'name',       header: 'Name',       width: 200 },
    { field: 'department', header: 'Department', width: 150 },
    { field: 'city',       header: 'City',       width: 140 },
    { field: 'age',        header: 'Age',        width: 90 },
    { field: 'salary',     header: 'Salary',     width: 130, format: { type: 'currency', currency: 'USD' } },
  ]
</script>
<script lang="ts">
  import { SvGrid, type ConditionalFormat } from '@svgrid/grid'

  type Row = { rep: string; revenue: number; score: number }

  const conditionalFormats: ConditionalFormat<Row>[] = [
    { type: 'dataBar', columns: ['revenue'], color: '#3b82f6' },
    { type: 'colorScale', columns: ['score'], min: '#fca5a5', mid: '#fde68a', max: '#86efac' },
  ]
</script>

<SvGrid {data} {columns} {conditionalFormats} />

Format kinds

colorScale - gradient fill

A 2-stop (min/max) or 3-stop (min/mid/max) gradient mapped across the column's value range. Fix the scale with minValue/maxValue to make rows comparable.

{ type: 'colorScale', columns: ['score'], min: '#fca5a5', mid: '#fde68a', max: '#86efac', minValue: 0, maxValue: 100 }

The color scale has several modes for turning a column into a live heat map:

Alpha ramp (mode: 'alpha') - keep a single base color and interpolate its opacity instead of its hue. Because the tint is translucent it composites over zebra striping, selection, and pinned backgrounds rather than painting over them - the cleanest heat-map look. Tune the opacity range with alphaBounds: [min, max] (default [0.05, 0.85]).

{ type: 'colorScale', columns: ['score'], mode: 'alpha', base: '#2563eb', alphaBounds: [0.08, 0.8] }

Zero-centred (zeroCentred: true) - a diverging scale pinned at 0: negatives and positives shade outward from a neutral midpoint, symmetric around zero. Ideal for P&L, price deltas, or day-over-day change.

{ type: 'colorScale', columns: ['pnl'], min: '#ef4444', mid: '#f8fafc', max: '#22c55e', zeroCentred: true }

Percent bounds (bounds: 'percent') - read minValue/maxValue as 0..100 positions along the column's own span, so you can tint "the top 20%" without knowing the numbers up front.

{ type: 'colorScale', columns: ['score'], min: '#f8fafc', max: '#2563eb', bounds: 'percent', minValue: 80, maxValue: 100 }

Banded / N-stop (stops) - supply any number of { offset, color } stops (offsets 0..1) for a traffic-light or multi-band ramp; this overrides min/mid/max.

{ type: 'colorScale', columns: ['risk'], stops: [
  { offset: 0, color: '#22c55e' }, { offset: 0.5, color: '#f59e0b' }, { offset: 1, color: '#ef4444' },
] }

Column comparison (compareColumn) - tint each cell by its value as a proportion of another field on the same row (filled vs target, open vs total) instead of the column extremes. No column stats needed.

{ type: 'colorScale', columns: ['filled'], mode: 'alpha', base: '#2563eb', compareColumn: 'target' }

Add reverse: true to flip any ramp so the lowest values draw the eye, and tooltip: 'value' | 'percent' to show the raw value or its position on the ramp on hover.

dataBar - in-cell bar

An in-cell horizontal bar proportional to the value. Diverging data (can go negative) gets negativeColor. showValue: false hides the text and shows the bar alone. gradient: true fills the bar with a left-to-right gradient.

{ type: 'dataBar', columns: ['revenue'], color: '#3b82f6', negativeColor: '#ef4444' }

Data bars accept the same relational range options as the color scale: bounds: 'percent' and compareColumn: 'target' size the bar against a percent of the span or another column on the row.

{ type: 'dataBar', columns: ['filled'], color: '#3b82f6', compareColumn: 'target' }

iconSet - threshold icons

An icon chosen by ascending thresholds (n thresholds => n+1 buckets). Built-in sets: 'arrows', 'traffic', 'triangles'. iconOnly: true hides the number.

// growth < 0 -> down, 0..10 -> flat, >= 10 -> up
{ type: 'iconSet', columns: ['growth'], set: 'arrows', thresholds: [0, 10] }

rule - style on a predicate

Apply background / color / fontWeight when when(ctx) returns true. The predicate receives the typed row, so you can key off other fields.

{ type: 'rule', columns: ['churn'], when: ({ value }) => Number(value) >= 20,
  background: '#fee2e2', color: '#991b1b', fontWeight: 700 }

Scoping and precedence

Stat scope

colorScale and dataBar scale against a column's min/max. conditionalStatScope picks which rows feed that range:

Value Rows scanned
filtered (default) Everything that survives the filters, ignoring the page slice.
visible Only the rows on screen, so each page normalizes to itself.
all The full unfiltered dataset, so the ramp stays put as you filter.

The default deliberately ignores paging: with a per-page range the same value renders one colour on page 1 and a different one on page 2, which makes the encoding misleading. Opt into visible when you actually want per-page normalization.

<SvGrid {data} {columns} {conditionalFormats} conditionalStatScope="all" />

Notes

See the live Conditional formatting demo.

More examples

Conditional formatting

Excel-style color scale, data bars, icon sets, heatmap tint - all via user-land cellRenderers. Per-formatter toggles to compare on/off.

Open the live example: Conditional formatting (Rows & Cells)

Anomaly highlights

IQR + rare-value detectors paint outliers per cell with severity halos (warning / outlier / extreme). Severity threshold toggle, per-detector tooltip explaining what tripped.

Open the live example: Anomaly highlights (Rows & Cells)

Live examples

  • Conditional formatting (engine) - Excel-style value-driven cell coloring as a declarative `conditionalFormats` engine prop: gradient heat maps (alpha ramp, zero-centred, banded, column-comparison), in-cell data bars, icon sets, and predicate rules - scoped per column, no per-cell snippet.
  • Conditional formatting - Excel-style color scale, data bars, icon sets, heatmap tint - all via user-land cellRenderers. Per-formatter toggles to compare on/off.
  • Anomaly highlights - IQR + rare-value detectors paint outliers per cell with severity halos (warning / outlier / extreme). Severity threshold toggle, per-detector tooltip explaining what tripped.

Related articles