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
classNamewith thecellscallback when you want reusable CSS classes driven by cell values. This is the recommended default. - Use the
rendereroption when you need inline styles or want to transform the displayed value. - Combine a
rendererwith 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.
/* 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 modulesregisterAllModules();
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ],};/* end-file */<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.
/* 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 signalconst 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 modulesregisterAllModules();
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ],};/* end-file */<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.
/* 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 visibleconst 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 modulesregisterAllModules();
export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), { provide: HOT_GLOBAL_CONFIG, useValue: { license: NON_COMMERCIAL_LICENSE } as HotGlobalConfig, }, ],};/* end-file */<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
lossclass 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 articles
Related guides
Configuration options