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.

Vue
<script setup lang="ts">
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import type { GridSettings } from 'handsontable/settings';
import type Handsontable from 'handsontable/base';
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 hotSettings: GridSettings = {
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 = {};
const value = data[row]?.[col];
if (col > 0) {
cellProperties.className = '';
if (typeof value === 'number' && value < 0) {
cellProperties.className = 'loss';
} else if (typeof value === 'number' && value > 10) {
cellProperties.className = 'strong-quarter';
}
}
return cellProperties;
},
};
</script>
<template>
<div id="example1">
<HotTable :settings="hotSettings" />
</div>
</template>
<style>
#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;
}
</style>

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.

Vue
<script setup lang="ts">
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import { registerRenderer } from 'handsontable/renderers';
import type { BaseRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
import type { GridSettings } from 'handsontable/settings';
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 hotSettings: GridSettings = {
data,
colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'],
licenseKey: 'non-commercial-and-evaluation',
height: 'auto',
columns: [
{},
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
{ renderer: 'profitRenderer' },
],
};
</script>
<template>
<div id="example2">
<HotTable :settings="hotSettings" />
</div>
</template>
<style>
#example2 .handsontable td.loss-cell {
color: #d81e2c;
font-weight: 600;
}
html[data-theme="dark"] #example2 .handsontable td.loss-cell {
color: #ff5c70;
}
</style>

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.

Vue
<script setup lang="ts">
import { HotTable } from '@handsontable/vue3';
import { registerAllModules } from 'handsontable/registry';
import { registerRenderer } from 'handsontable/renderers';
import type { BaseRenderer } from 'handsontable/renderers';
import { textRenderer } from 'handsontable/renderers/textRenderer';
import type { GridSettings } from 'handsontable/settings';
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 hotSettings: GridSettings = {
data,
colHeaders: ['Company', 'Q1', 'Q2', 'Q3', 'Q4'],
licenseKey: 'non-commercial-and-evaluation',
height: 'auto',
columns: [
{},
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
{ renderer: 'heatmapRenderer' },
],
};
</script>
<template>
<div id="example3">
<HotTable :settings="hotSettings" />
</div>
</template>

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