Theme color testing

The colors of a chart theme have more jobs than the colors of buttons and text. Readers have to tell the series apart from the page, and from each other. Some charts print labels in the series colors, others put labels on top of the series. All of that has to work for readers with a color vision deficiency too.

This page checks the colors of a theme for all of these at once. Start from a built-in theme, change the series colors, the page background or the label color, and see the results, with what each failure means and which color would fix it. The same checks run in unit tests with @recharts/devtools/theme, which takes your RechartsTheme as it is, so you can keep your theme readable as it changes. Every threshold links to the standard that defines it, and where no standard exists, this page says so.

Theme checker

Theme

Start from:

Series colors, graphicalItems

  1. 1.
  2. 2.
  3. 3.
  4. 4.
  5. 5.
  6. 6.
  7. 7.
  8. 8.
  9. 9.
  10. 10.
  11. 11.
  12. 12.

Page and labels

Page background, pageBackground
Label color, typography.color

Requirements

Results

✗ 17 checks fail:

  • Series 1, 2, 6, 8, 10, 11 and 12 are too close to the page color to use for label text, for readers with a color vision deficiency.
  • Labels are hard to read inside series 4, 7 and 9.

Changes series and label colors to the suggested ones, a few rounds at a time, and keeps the round with the fewest failures. Labels inside series and touching series need other fixes, described below.

How to read the numbers. A contrast ratio runs from 1:1, two identical colors, to 21:1, black on white. A ΔE00 color difference is 0 for identical colors and grows as they get easier to tell apart. ✓ means the value meets its threshold, ✗ means it falls short, by how much, and which nearby color would pass. A grey value has no threshold and is informational.

Summary: the worst value of each check

CheckNeedsStandardTypical visionProtanopiaDeuteranopiaTritanopia
Series on the page
seriesOnBackground
3:1 or moreWCAG SC 1.4.11✓ 3.42:1✓ 3.13:1✓ 3.29:1✓ 3.20:1
Series against each other (ΔE00)
seriesVsSeries
ΔE00 10 or moreCIEDE2000✓ 10.6✓ 3.4✓ 3.6✓ 3.4
Series against each other (contrast)
seriesContrast
no thresholdWCAG SC 1.4.111.00:11.00:11.00:11.00:1
Labels on the page
textOnBackground
4.5:1 or moreWCAG SC 1.4.3✓ 20.87:1✓ 20.87:1✓ 20.87:1✓ 20.87:1
Series color as label text
seriesAsText
4.5:1 or moreWCAG SC 1.4.3✓ 4.50:1✗ 3.91:1, 0.59 short (4 failed)✗ 4.21:1, 0.29 short (3 failed)✗ 3.85:1, 0.65 short (3 failed)
Labels on series fills
textOnSeries
4.5:1 or moreWCAG SC 1.4.3✗ 3.72:1, 0.78 short (1 failed)✗ 3.55:1, 0.95 short (3 failed)✗ 3.83:1, 0.67 short (1 failed)✗ 3.80:1, 0.70 short (2 failed)

What the failures mean and what to change

✗ Series color as label text: 10 below 4.5:1 or more

What it means: Labels drawn in the series color, like the default pie chart labels, are hard to read.

What to do: Make the series color darker on a light page, or lighter on a dark page. Or draw those labels in the label color instead of the series color.

  • series 1 (strokes), protanopia: 3.91:1, 0.59 short. try series 1 stroke #1568da
  • series 6 (strokes), protanopia: 4.05:1, 0.45 short. try series 6 stroke #007997
  • series 8 (strokes), protanopia: 4.42:1, 0.08 short. try series 8 stroke #607e37
  • series 10 (strokes), protanopia: 4.30:1, 0.20 short. try series 10 stroke #357c78
  • series 2 (strokes), deuteranopia: 4.32:1, 0.18 short. try series 2 stroke #b15400
  • series 11 (strokes), deuteranopia: 4.26:1, 0.24 short. try series 11 stroke #976a23
  • series 12 (strokes), deuteranopia: 4.21:1, 0.29 short. try series 12 stroke #a55599
  • series 1 (strokes), tritanopia: 3.85:1, 0.65 short. try series 1 stroke #1366d8

And 2 more. Fix these first, then check again.

✗ Labels on series fills: 7 below 4.5:1 or more

What it means: Labels inside bars or pie slices are hard to read on that series.

What to do: Use a different label color for labels inside this series, move the labels outside, or change the series lightness so the label color stands out from it.

  • series 9 (fills), typical vision: 3.72:1, 0.78 short. for labels inside this series, use #e6e6e6
  • series 4 (fills), protanopia: 3.93:1, 0.57 short. for labels inside this series, use #ededed
  • series 7 (fills), protanopia: 3.67:1, 0.83 short. for labels inside this series, use #e5e5e5
  • series 9 (fills), protanopia: 3.55:1, 0.95 short. for labels inside this series, use #e1e1e1
  • series 9 (fills), deuteranopia: 3.83:1, 0.67 short. for labels inside this series, use #eaeaea
  • series 7 (fills), tritanopia: 4.48:1, 0.02 short. for labels inside this series, use #000000
  • series 9 (fills), tritanopia: 3.80:1, 0.70 short. for labels inside this series, use #e9e9e9

The suggested colors keep the hue and change only the lightness, as little as possible. Each one fixes the check it is listed under, and may affect other checks, so apply them one at a time and look at the results again.

Each series

SeriesFill on the page
needs 3:1
Stroke on the page
needs 3:1
As label text
needs 4.5:1
Label on the fill
needs 4.5:1
1 #2373e6✓ 3.53:1✓ 4.50:1Label ✓ 4.50:1Label ✓ 5.92:1
2 #b65700✓ 3.74:1✓ 4.83:1Label ✓ 4.83:1Label ✓ 5.58:1
3 #017a58✓ 4.04:1✓ 5.35:1Label ✓ 5.35:1Label ✓ 5.16:1
4 #a5386e✓ 4.58:1✓ 6.19:1Label ✓ 6.19:1Label ✓ 4.55:1
5 #6b53b0✓ 4.33:1✓ 6.02:1Label ✓ 6.02:1Label ✓ 4.82:1
6 #0681a0✓ 3.53:1✓ 4.51:1Label ✓ 4.51:1Label ✓ 5.91:1
7 #b13629✓ 4.64:1✓ 6.12:1Label ✓ 6.12:1Label ✓ 4.50:1
8 #628039✓ 3.42:1✓ 4.50:1Label ✓ 4.50:1Label ✓ 6.10:1
9 #723683✓ 5.62:1✓ 8.17:1Label ✓ 8.17:1Label ✗ 3.72:1, 0.78 short for labels inside this series, use #e6e6e6
10 #39807c✓ 3.51:1✓ 4.61:1Label ✓ 4.61:1Label ✓ 5.95:1
11 #9b6e27✓ 3.44:1✓ 4.51:1Label ✓ 4.51:1Label ✓ 6.06:1
12 #aa5a9e✓ 3.46:1✓ 4.50:1Label ✓ 4.50:1Label ✓ 6.03:1

Every pair of series

Each cell is the CIEDE2000 difference between two series, as seen with typical vision. Your requirement is ΔE00 10 or more. A failing pair may be mixed up by readers: change the lightness or the hue of one of the two, or move them apart in the order of the theme.

Series123456789101112
1✓ 49.0✓ 37.6✓ 35.6✓ 18.6✓ 18.0✓ 42.5✓ 50.8✓ 29.5✓ 27.2✓ 47.7✓ 30.1
2✓ 43.8✓ 37.2✓ 45.4✓ 44.3✓ 19.1✓ 32.7✓ 44.2✓ 39.0✓ 11.5✓ 40.5
3✓ 60.1✓ 38.6✓ 22.1✓ 54.2✓ 16.3✓ 41.6✓ 11.4✓ 32.6✓ 42.7
4✓ 19.7✓ 50.5✓ 19.5✓ 53.7✓ 13.0✓ 39.0✓ 40.3✓ 10.6
5✓ 27.3✓ 33.3✓ 45.7✓ 11.2✓ 29.5✓ 46.4✓ 14.6
6✓ 54.4✓ 33.7✓ 37.7✓ 11.0✓ 38.9✓ 41.3
7✓ 44.7✓ 28.8✓ 44.9✓ 25.8✓ 26.2
8✓ 56.6✓ 20.5✓ 20.3✓ 53.7
9✓ 34.0✓ 46.4✓ 14.4
10✓ 31.2✓ 33.7
11✓ 42.4
12

The theme in charts

The same theme, as seen with the vision selected above. Axis ticks, axis titles and legends use the label color on the page. The bars carry labels in the label color, and the pie labels use the series colors. Hover a chart to see its tooltip.

The same checks as a unit test

analyzeTheme takes your RechartsTheme object as it is. This test rebuilds the theme you edited above; in your project, import your own theme instead.

import { lightTheme, type RechartsTheme } from 'recharts';
import { analyzeTheme, themeFailures } from '@recharts/devtools/theme';

// In your project, import your own theme instead.
const myTheme: RechartsTheme = {
  ...lightTheme,
  pageBackground: '#fff',
  typography: { ...lightTheme.typography, color: '#010101' },
  graphicalItems: [
    { fill: '#2373e6', stroke: '#2373e6', fillOpacity: 0.85 },
    { fill: '#b65700', stroke: '#b65700', fillOpacity: 0.85 },
    { fill: '#017a58', stroke: '#017a58', fillOpacity: 0.85 },
    { fill: '#a5386e', stroke: '#a5386e', fillOpacity: 0.85 },
    { fill: '#6b53b0', stroke: '#6b53b0', fillOpacity: 0.85 },
    { fill: '#0681a0', stroke: '#0681a0', fillOpacity: 0.85 },
    { fill: '#b13629', stroke: '#b13629', fillOpacity: 0.85 },
    { fill: '#628039', stroke: '#628039', fillOpacity: 0.85 },
    { fill: '#723683', stroke: '#723683', fillOpacity: 0.85 },
    { fill: '#39807c', stroke: '#39807c', fillOpacity: 0.85 },
    { fill: '#9b6e27', stroke: '#9b6e27', fillOpacity: 0.85 },
    { fill: '#aa5a9e', stroke: '#aa5a9e', fillOpacity: 0.85 },
  ],
};

test('my theme is readable', () => {
  const report = analyzeTheme(myTheme, {
    severity: 1,
    thresholds: { seriesDifference: 10, seriesDifferenceCvd: 2 },
  });
  expect(themeFailures(report)).toEqual([]);
});

What each check measures

CheckWhat it comparesDefault threshold
seriesOnBackgroundEach series against the page: the fill painted at its fillOpacity, and the opaque stroke.3:1, WCAG 2.2 SC 1.4.11 Non-text Contrast
seriesVsSeriesThe CIEDE2000 color difference (ΔE00) between every pair of series.None, you choose. See below.
seriesContrastThe WCAG contrast ratio between every pair of series, for marks that touch, like pie slices.None, you choose. See below.
textOnBackgroundThe label color, typography.color, on the page.4.5:1, WCAG 2.2 SC 1.4.3 Contrast (Minimum)
seriesAsTextA series color used for label text on the page, like the default pie labels.4.5:1, SC 1.4.3
textOnSeriesThe label color on each series fill, like labels inside bars.4.5:1, SC 1.4.3

Every check runs for every page background, and for typical vision plus three simulated color vision deficiencies.

Contrast

WCAG 2.2 sets contrast ratios between 1:1 and 21:1, computed from the relative luminance of the two colors, as its contrast ratio definition describes. Three success criteria matter for charts:

The defaults are the level AA numbers for normal text. The WCAG_CONTRAST export holds the others, and you can pass any of them as thresholds.textContrast.

The Understanding document for SC 1.4.11 has examples for charts. In a line chart, the lines need 3:1 against the background, but not against each other when they rarely overlap. In a pie chart, readers have to tell the slices apart, so neighboring slices need 3:1 against each other, unless something else separates them, like a contrasting border, or labels that carry the same information. That is what the seriesContrast check measures. It has no default threshold, because whether it applies depends on your chart: in the checker, turn on "My series touch each other" to require 3:1, and in a unit test pass thresholds: { seriesContrast: 3 }.

Telling series apart

No standard defines how different two series colors must be. WCAG asks for something else instead: SC 1.4.1 Use of Color says color must not be the only visual means of conveying information. Direct labels, different markers or dash patterns give readers another way to tell series apart.

Color difference still matters: the closer two colors are, the more often readers mix them up. The seriesVsSeries check measures it with CIEDE2000, the color difference formula of the CIE, standardized as ISO/CIE 11664-6. Gaurav Sharma publishes implementation notes and test data for it, and the devtools test suite checks against that data.

How large a difference is large enough depends on the chart. In Modeling Color Difference for Visualization Design (IEEE TVCG, 2018), Danielle Albers Szafir found that people's ability to perceive color differences varies significantly across mark types: points, bars and lines. That is why seriesDifference and seriesDifferenceCvd have no default. The checker above starts with the numbers the built-in Recharts themes are tested with: ΔE00 10 in typical vision and 2 under simulation. These are project choices, not standards.

Color vision deficiencies

The checks simulate protanopia, deuteranopia and tritanopia with the model of Machado, Oliveira and Fernandes, A Physiologically-based Model for Simulation of Color Vision Deficiency (IEEE TVCG, 2009, doi:10.1109/TVCG.2009.113). A severity below 1 approximates anomalous trichromacy, a milder form. The model is published in steps of 0.1.

The model mixes amounts of light, so it applies to linear RGB, not to the gamma-encoded values of a hex color. The R package colorspace fixed this in version 2.1-0. The difference is small for muted colors and noticeable for saturated reds, purples and oranges.

A simulation approximates what some readers see. It does not replace testing with them. Also note that WCAG defines contrast for the colors as they are displayed. The contrast checks under simulation are an extra warning sign, not a WCAG requirement: turn them off by passing visions: ['normal'] to a separate analyzeTheme call.

How colors are measured

Colors are sRGB, as CSS Color Module Level 4 defines them. A translucent fill is first painted over the page with simple alpha compositing, which gives the color the reader actually sees, and then the color is simulated and measured.

All the color math comes from culori: parsing, compositing, WCAG contrast, CIEDE2000, the simulation matrices and the OKLCH lightness that the suggested fixes move along. The theme module only combines them.

Testing your own theme

Install @recharts/devtools as a development dependency. analyzeTheme takes your RechartsTheme object as it is and reads its graphicalItems, typography.color and pageBackground. Nothing from React or Recharts runs: the theme is plain data, and Recharts 3.11 or newer only provides its type. The @recharts/devtools/theme entry point runs in any test runner that supports ES modules.

import { analyzeTheme, themeFailures } from '@recharts/devtools/theme';
import { myTheme } from './myTheme';

test('my theme is readable', () => {
  const report = analyzeTheme(myTheme, {
    // No standard defines these two. Pick numbers that fit your marks, see "Telling series apart".
    thresholds: { seriesDifference: 10, seriesDifferenceCvd: 2 },
    // Our labels sit outside the bars, never on top of a series fill.
    checks: { textOnSeries: false },
  });
  expect(themeFailures(report)).toEqual([]);
});

By default the checks run on the theme's pageBackground. Pass other backgrounds when the theme shows on more than one surface:

// The theme also shows on cards, so check both surfaces.
analyzeTheme(myTheme, { backgrounds: { page: '#ffffff', card: '#f4f4f5' } });

// A light and a dark theme are two themes: check each one.
analyzeTheme(myLightTheme);
analyzeTheme(myDarkTheme);

analyzeTheme returns plain data: every measured value with its threshold, its standard and, when it fails, the nearest color that would pass. themeFailures turns the failed checks into one readable line each, fix included. See the Theming guide for how to write a theme.

Which standard applies to you

WCAG is the reference for most accessibility laws and standards, in one version or another. According to the W3C WCAG overview, WCAG 2.2 is also the international standard ISO/IEC 40500:2025, and EN 301 549, the European standard used for the European Accessibility Act, uses WCAG 2.2 in its 2026 version. In the United States, the Section 508 standards incorporate WCAG 2.0 level A and AA, and the ADA Title II rule adopts WCAG 2.1 level AA. Check which version applies to you: this page links WCAG 2.2.

WCAG 3 is an early draft, not a standard yet, so this page does not use it.