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.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
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 ExampleComponent = () => {
return (<HotTable data={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={function (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;
}}/>);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import Handsontable from 'handsontable/base';
import { registerAllModules } from 'handsontable/registry';
// register Handsontable's modules
registerAllModules();
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 ExampleComponent = () => {
return (
<HotTable
data={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={function (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;
}}
/>
);
};
export default ExampleComponent;
CSS
#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.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { registerRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
// register Handsontable's modules
registerAllModules();
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 signal
const 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 ExampleComponent = () => {
return (<HotTable data={data} colHeaders={['Company', 'Q1', 'Q2', 'Q3', 'Q4']} licenseKey="non-commercial-and-evaluation" height="auto" columns={[
{},
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
]}/>);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { BaseRenderer, registerRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
// register Handsontable's modules
registerAllModules();
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 signal
const 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 ExampleComponent = () => {
return (
<HotTable
data={data}
colHeaders={['Company', 'Q1', 'Q2', 'Q3', 'Q4']}
licenseKey="non-commercial-and-evaluation"
height="auto"
columns={[
{},
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
]}
/>
);
};
export default ExampleComponent;
CSS
#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.

JavaScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { registerRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
// register Handsontable's modules
registerAllModules();
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, 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 ExampleComponent = () => {
return (<HotTable data={data} colHeaders={['Company', 'Q1', 'Q2', 'Q3', 'Q4']} licenseKey="non-commercial-and-evaluation" height="auto" columns={[
{},
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
]}/>);
};
export default ExampleComponent;
TypeScript
import { HotTable } from '@handsontable/react-wrapper';
import { registerAllModules } from 'handsontable/registry';
import { BaseRenderer, registerRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
// register Handsontable's modules
registerAllModules();
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: 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 ExampleComponent = () => {
return (
<HotTable
data={data}
colHeaders={['Company', 'Q1', 'Q2', 'Q3', 'Q4']}
licenseKey="non-commercial-and-evaluation"
height="auto"
columns={[
{},
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
]}
/>
);
};
export default ExampleComponent;

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