Skip to content

Conditional formatting

Style cells dynamically based on their values, so you can highlight outliers, flag errors, or visualize data ranges.

Overview

Conditional formatting changes a cell’s appearance based on the data it holds. As values change, the formatting updates on the next render.

Choose an approach based on your goal:

  • Use className with the cells callback when you want reusable CSS classes driven by cell values. This is the recommended default.
  • Use the renderer option when you need inline styles or want to transform the displayed value.
  • Combine a renderer with a computed background color when you want a color scale that reflects each value’s magnitude.

For static styling that does not depend on cell values, see Formatting cells.

Prerequisites

  • A Handsontable instance with data loaded.
  • A stylesheet, if you format cells through custom CSS classes.
  • Familiarity with the cascading configuration model, where cell settings override column settings, which override global settings. See Configuration options.

Highlight cells by value with a CSS class

Return a className from the cells callback to style cells by value. The callback runs for every cell, so you can inspect the value and assign a class conditionally.

This example uses the cascading model on two levels: the Company column sets a static className through its columns entry, while the cells callback adds a loss class to negative quarters and a strong-quarter class to quarters above 10. The loss class pairs its red color with a leading marker, so the value stays readable without color.

TypeScript
/* file: app.component.ts */
import { Component, ViewEncapsulation } from '@angular/core';
import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper';
import Handsontable from 'handsontable/base';
const data = [
['Acme Corp', 4.2, 5.1, -1.3, 6.8],
['Vertex Industries', 12.5, 11.9, 13.2, 14],
['Harbor Analytics', -2.4, 0.8, 2.1, 3.5],
['Summit Logistics', 8.7, -3.2, 4.4, 5.9],
['Pioneer Foods', 1.1, 1.4, 0.9, -0.5],
['Meridian Retail', 6, 7.3, 8.1, 9.4],
];
@Component({
selector: 'example1-conditional-formatting',
standalone: true,
imports: [HotTableModule],
template: ` <div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>`,
styles: `example1-conditional-formatting .handsontable td.company-name {
font-weight: 600;
}
example1-conditional-formatting .handsontable td.loss {
color: #d81e2c;
background: var(--ht-cell-error-background-color, rgba(216, 30, 44, 0.14));
background: color-mix(in srgb, #d81e2c 16%, var(--ht-background-color, #ffffff));
}
example1-conditional-formatting .handsontable td.loss::before {
content: "▼ ";
}
example1-conditional-formatting .handsontable td.strong-quarter {
color: #157a3d;
font-weight: 600;
}
html[data-theme="dark"] example1-conditional-formatting .handsontable td.loss {
color: #ff5c70;
background: color-mix(in srgb, #ff5c70 22%, var(--ht-background-color, #000000));
}
html[data-theme="dark"] example1-conditional-formatting .handsontable td.strong-quarter {
color: #22c55e;
}
`,
encapsulation: ViewEncapsulation.None,
})
export class AppComponent {
readonly data = data;
readonly gridSettings: GridSettings = {
colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'],
height: 'auto',
columns: [
{ className: 'company-name' },
{ type: 'numeric' },
{ type: 'numeric' },
{ type: 'numeric' },
{ type: 'numeric' },
],
cells(row: number, col: number) {
const cellProperties: Handsontable.CellMeta = {};
if (col > 0) {
cellProperties.className = '';
const value = data[row]?.[col];
if (typeof value === 'number' && value < 0) {
cellProperties.className = 'loss';
} else if (typeof value === 'number' && value > 10) {
cellProperties.className = 'strong-quarter';
}
}
return cellProperties;
},
};
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div>
<example1-conditional-formatting></example1-conditional-formatting>
</div>

Format cells with a custom renderer

Use a renderer when a CSS class is not enough - for example, when you need to transform the displayed value or apply inline styles computed at render time.

This renderer formats each quarter as a currency amount and shows losses in an accounting format, ($1.3M). The parentheses convey a loss on their own, so the red color is a secondary signal.

TypeScript
/* file: app.component.ts */
import { Component, ViewEncapsulation } from '@angular/core';
import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper';
import Handsontable from 'handsontable/base';
import { registerRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
// display losses in an accounting format, so color is not the only signal
const profitRenderer = (
instance: Handsontable,
td: HTMLTableCellElement,
row: number,
col: number,
prop: string | number,
value: Handsontable.CellValue,
cellProperties: Handsontable.CellProperties
) => {
const amount = Number(value);
td.classList.remove('loss-cell');
if (!Number.isFinite(amount)) {
textRenderer(instance, td, row, col, prop, value, cellProperties);
return;
}
const formatted = amount < 0
? `($${Math.abs(amount).toFixed(1)}M)`
: `$${amount.toFixed(1)}M`;
textRenderer(instance, td, row, col, prop, formatted, cellProperties);
if (amount < 0) {
td.classList.add('loss-cell');
}
};
registerRenderer('profitRenderer', profitRenderer);
@Component({
selector: 'example2-conditional-formatting',
standalone: true,
imports: [HotTableModule],
template: ` <div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>`,
styles: `example2-conditional-formatting .handsontable td.loss-cell {
color: #d81e2c;
font-weight: 600;
}
html[data-theme="dark"] example2-conditional-formatting .handsontable td.loss-cell {
color: #ff5c70;
}
`,
encapsulation: ViewEncapsulation.None,
})
export class AppComponent {
readonly data = [
['Acme Corp', 4.2, 5.1, -1.3, 6.8],
['Vertex Industries', 12.5, 11.9, 13.2, 14],
['Harbor Analytics', -2.4, 0.8, 2.1, 3.5],
['Summit Logistics', 8.7, -3.2, 4.4, 5.9],
['Pioneer Foods', 1.1, 1.4, 0.9, -0.5],
['Meridian Retail', 6, 7.3, 8.1, 9.4],
];
readonly gridSettings: GridSettings = {
colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'],
height: 'auto',
columns: [
{},
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
],
};
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div>
<example2-conditional-formatting></example2-conditional-formatting>
</div>

Build a color scale

To visualize magnitude, compute a background color from each value and apply it in a renderer. This example maps every quarter to a green-to-red scale based on the minimum and maximum across the data, producing a heatmap. The numeric value remains visible in each cell, so the color adds meaning rather than replacing it.

TypeScript
/* file: app.component.ts */
import { Component } from '@angular/core';
import { GridSettings, HotTableModule } from '@handsontable/angular-wrapper';
import Handsontable from 'handsontable/base';
import { registerRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
const data = [
['Acme Corp', 4.2, 5.1, -1.3, 6.8],
['Vertex Industries', 12.5, 11.9, 13.2, 14],
['Harbor Analytics', -2.4, 0.8, 2.1, 3.5],
['Summit Logistics', 8.7, -3.2, 4.4, 5.9],
['Pioneer Foods', 1.1, 1.4, 0.9, -0.5],
['Meridian Retail', 6, 7.3, 8.1, 9.4],
];
// shade the background from red (low) to green (high); the value stays visible
const heatmapRenderer = (
instance: Handsontable,
td: HTMLTableCellElement,
row: number,
col: number,
prop: string | number,
value: Handsontable.CellValue,
cellProperties: Handsontable.CellProperties
) => {
textRenderer(instance, td, row, col, prop, value, cellProperties);
const amount = Number(value);
if (Number.isFinite(amount)) {
const values = instance.getData()
.flatMap((rowData) => rowData.slice(1))
.map(Number)
.filter((cellValue) => Number.isFinite(cellValue));
const min = Math.min(...values);
const max = Math.max(...values);
const ratio = min === max ? 0.5 : (amount - min) / (max - min);
const hue = Math.round(ratio * 120);
td.style.background = `hsl(${hue}, 75%, 85%)`;
td.style.color = '#1b1b1b';
}
};
registerRenderer('heatmapRenderer', heatmapRenderer);
@Component({
selector: 'example3-conditional-formatting',
standalone: true,
imports: [HotTableModule],
template: ` <div>
<hot-table [data]="data" [settings]="gridSettings"></hot-table>
</div>`,
})
export class AppComponent {
readonly data = data;
readonly gridSettings: GridSettings = {
colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'],
height: 'auto',
columns: [
{},
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
],
};
}
/* end-file */
/* file: app.config.ts */
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { registerAllModules } from 'handsontable/registry';
import { HOT_GLOBAL_CONFIG, HotGlobalConfig, NON_COMMERCIAL_LICENSE } from '@handsontable/angular-wrapper';
// register Handsontable's modules
registerAllModules();
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
{
provide: HOT_GLOBAL_CONFIG,
useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig,
},
],
};
/* end-file */
HTML
<div>
<example3-conditional-formatting></example3-conditional-formatting>
</div>

Make conditional formatting accessible

Color alone does not meet WCAG 2.1 AA. Readers with color vision deficiencies, or anyone viewing the grid in high-contrast mode, can miss a signal that relies only on color.

  • Pair color with a second cue: a symbol, text label, or font weight. In the first example, the loss class adds a marker; in the second, losses appear in parentheses.
  • In a color scale, keep the underlying value visible so the color stays supplementary.
  • Verify that text keeps a contrast ratio of at least 4.5:1 against the cell background you apply.

Result

Cells that match your conditions display the configured styles - classes, inline styles, or a color scale - and update automatically as the data changes.

Related guides

Configuration options