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.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';// register Handsontable's modulesregisterAllModules();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],];const container = document.querySelector('#example1');new Handsontable(container, { data, colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'], licenseKey: 'non-commercial-and-evaluation', height: 'auto', columns: [ { className: 'company-name' }, { type: 'numeric' }, { type: 'numeric' }, { type: 'numeric' }, { type: 'numeric' }, ], cells(row, col) { const cellProperties = {}; 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; },});import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modulesregisterAllModules();
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],];
const container = document.querySelector('#example1')!;
new Handsontable(container, { data, colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'], licenseKey: 'non-commercial-and-evaluation', height: 'auto', columns: [ { className: 'company-name' }, { type: 'numeric' }, { type: 'numeric' }, { type: 'numeric' }, { type: 'numeric' }, ], cells(row, col) { 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; },});#example1 .handsontable td.company-name { font-weight: 600;}
#example1 .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 .handsontable td.loss::before { content: "▼ ";}
#example1 .handsontable td.strong-quarter { color: #157a3d; font-weight: 600;}
html[data-theme="dark"] #example1 .handsontable td.loss { color: #ff5c70; background: color-mix(in srgb, #ff5c70 22%, var(--ht-background-color, #000000));}
html[data-theme="dark"] #example1 .handsontable td.strong-quarter { color: #22c55e;}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.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { registerRenderer } from 'handsontable/renderers';import { textRenderer } from 'handsontable/renderers/textRenderer';// register Handsontable's modulesregisterAllModules();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],];// display losses in an accounting format, so color is not the only signalconst profitRenderer = (instance, td, row, col, prop, value, 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);const container = document.querySelector('#example2');new Handsontable(container, { data, colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'], licenseKey: 'non-commercial-and-evaluation', height: 'auto', columns: [ {}, { renderer: 'profitRenderer' }, { renderer: 'profitRenderer' }, { renderer: 'profitRenderer' }, { renderer: 'profitRenderer' }, ],});import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { BaseRenderer, registerRenderer } from 'handsontable/renderers';import { textRenderer } from 'handsontable/renderers/textRenderer';
// register Handsontable's modulesregisterAllModules();
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],];
// display losses in an accounting format, so color is not the only signalconst profitRenderer: BaseRenderer = (instance, td, row, col, prop, value, 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);
const container = document.querySelector('#example2')!;
new Handsontable(container, { data, colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'], licenseKey: 'non-commercial-and-evaluation', height: 'auto', columns: [ {}, { renderer: 'profitRenderer' }, { renderer: 'profitRenderer' }, { renderer: 'profitRenderer' }, { renderer: 'profitRenderer' }, ],});#example2 .handsontable td.loss-cell { color: #d81e2c; font-weight: 600;}
html[data-theme="dark"] #example2 .handsontable td.loss-cell { color: #ff5c70;}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.
import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { registerRenderer } from 'handsontable/renderers';import { textRenderer } from 'handsontable/renderers/textRenderer';// register Handsontable's modulesregisterAllModules();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, td, row, col, prop, value, 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);const container = document.querySelector('#example3');new Handsontable(container, { data, colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'], licenseKey: 'non-commercial-and-evaluation', height: 'auto', columns: [ {}, { renderer: 'heatmapRenderer' }, { renderer: 'heatmapRenderer' }, { renderer: 'heatmapRenderer' }, { renderer: 'heatmapRenderer' }, ],});import Handsontable from 'handsontable/base';import { registerAllModules } from 'handsontable/registry';import { BaseRenderer, registerRenderer } from 'handsontable/renderers';import { textRenderer } from 'handsontable/renderers/textRenderer';
// register Handsontable's modulesregisterAllModules();
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: BaseRenderer = (instance, td, row, col, prop, value, 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);
const container = document.querySelector('#example3')!;
new Handsontable(container, { data, colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'], licenseKey: 'non-commercial-and-evaluation', height: 'auto', columns: [ {}, { renderer: 'heatmapRenderer' }, { renderer: 'heatmapRenderer' }, { renderer: 'heatmapRenderer' }, { renderer: 'heatmapRenderer' }, ],});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