OKLCH Studio · Accessible palettes · Data visualisation
Accessible heatmap and sequential colour scale: nine steps
Updated 2026-10-19
A sequential scale encodes a quantity that goes from low to high: counts, rates, temperatures above zero, the share of a population. The cardinal rule is that the order must be obvious, which means lightness has to rise in one direction. Rainbow scales break that rule, because the eye sees no consistent order in the hues.
This nine-step scale rises monotonically in OKLCH lightness, and the tests check it step by step. It also does something many scales do not: its lightest step still meets 3:1 against the chart background, so a low value does not disappear into the page. That makes empty-looking cells a deliberate no-data choice rather than a side effect.
On the light theme higher values are darker; on the dark theme higher values are lighter, so that in both cases a stronger value stands out more from the background. Neighbouring steps are checked under simulated protanopia, deuteranopia and tritanopia against a minimum difference, so the order stays readable.
How to build a palette like this by hand is explained in the accessible colour palette guide; this page is the finished, verified result.
The palette: light and dark
Hex values are the sRGB result after gamut mapping. The oklch() values are those of the final hex, so converting either gives the same colour.
Light chart: steps, with contrast on the chart background
- 1. #7C93AC3.07:1
- 2. #6286A33.73:1
- 3. #467A994.52:1
- 4. #226E8D5.53:1
- 5. #00627D6.70:1
- 6. #00566B8.01:1
- 7. #0049579.75:1
- 8. #003C4311.80:1
- 9. #002E3014.22:1
Light theme
- Chart background
#F9FCFF
oklch(0.990 0.005 247.9)
Sample text
- Chart text
#1D2022
oklch(0.241 0.006 236.8)
Sample text
- Axis labels
#55585C
oklch(0.459 0.008 255.5)
Sample text
- Axis line
#83878B
oklch(0.621 0.008 247.9)
Sample text
- Gridline
#CCD0D5
oklch(0.856 0.008 253.9)
Sample text
- Step 1
#7C93AC
oklch(0.654 0.046 250.7)
Sample text
- Step 2
#6286A3
oklch(0.604 0.060 243.3)
Sample text
- Step 3
#467A99
oklch(0.556 0.074 236.3)
Sample text
- Step 4
#226E8D
oklch(0.507 0.087 229.6)
Sample text
- Step 5
#00627D
oklch(0.4615 0.0874 225.00)
Sample text
- Step 6
#00566B
oklch(0.4199 0.0774 221.72)
Sample text
- Step 7
#004957
oklch(0.3735 0.0664 216.07)
Sample text
- Step 8
#003C43
oklch(0.326 0.056 207.7)
Sample text
- Step 9
#002E30
oklch(0.274 0.047 199.4)
Sample text
Dark chart: steps, with contrast on the chart background
- 1. #50667D3.05:1
- 2. #5477933.83:1
- 3. #5589A94.78:1
- 4. #549CBC5.92:1
- 5. #56AFCD7.26:1
- 6. #5FC2DA8.82:1
- 7. #70D4E410.56:1
- 8. #88E5EC12.49:1
- 9. #A6F5F514.68:1
Dark theme
- Chart background
#141618
oklch(0.199 0.005 248.1)
Sample text
- Chart text
#E8EBEF
oklch(0.939 0.006 255.5)
Sample text
- Axis labels
#AEB1B6
oklch(0.760 0.008 260.7)
Sample text
- Axis line
#717579
oklch(0.560 0.008 248.0)
Sample text
- Gridline
#33373A
oklch(0.334 0.008 240.1)
Sample text
- Step 1
#50667D
oklch(0.502 0.045 249.9)
Sample text
- Step 2
#547793
oklch(0.554 0.059 243.2)
Sample text
- Step 3
#5589A9
oklch(0.606 0.074 236.8)
Sample text
- Step 4
#549CBC
oklch(0.6586 0.0864 228.98)
Sample text
- Step 5
#56AFCD
oklch(0.711 0.095 222.7)
Sample text
- Step 6
#5FC2DA
oklch(0.7636 0.0984 215.86)
Sample text
- Step 7
#70D4E4
oklch(0.815 0.096 209.0)
Sample text
- Step 8
#88E5EC
oklch(0.867 0.089 201.9)
Sample text
- Step 9
#A6F5F5
oklch(0.919 0.077 195.9)
Sample text
Contrast table: every verified pair
Pass or fail uses the raw WCAG 2.x ratio; the ratio shown is floored to two decimals so a near miss is never rounded up. APCA Lc is signed: positive is dark on light, negative is light on dark. Each row opens the pair in the checker.
| Pair | WCAG ratio | Target | Normal AA / AAA | Large AA / AAA | APCA Lc | APCA tier | Check |
|---|---|---|---|---|---|---|---|
| Chart text on background#1d2022 on #f9fcff | 15.91:1 | AA 4.5:1 | Pass / Pass | Pass / Pass | +101.3 | Lc 75: body text | Open in checker |
| Axis labels on background#55585c on #f9fcff | 6.94:1 | AA 4.5:1 | Pass / Fail | Pass / Pass | +82.8 | Lc 75: body text | Open in checker |
| Axis line on background#83878b on #f9fcff | 3.51:1 | 3:1 (WCAG 1.4.11) | Fail / Fail | Pass / Fail | +61.8 | Lc 60: larger content text | Open in checker |
| Step 1 on background#7c93ac on #f9fcff | 3.07:1 | 3:1 (WCAG 1.4.11) | Fail / Fail | Pass / Fail | +56.8 | Lc 45: headlines and large text | Open in checker |
| Step 2 on background#6286a3 on #f9fcff | 3.73:1 | 3:1 (WCAG 1.4.11) | Fail / Fail | Pass / Fail | +63.9 | Lc 60: larger content text | Open in checker |
| Step 3 on background#467a99 on #f9fcff | 4.52:1 | 3:1 (WCAG 1.4.11) | Pass / Fail | Pass / Pass | +70.2 | Lc 60: larger content text | Open in checker |
| Step 4 on background#226e8d on #f9fcff | 5.53:1 | 3:1 (WCAG 1.4.11) | Pass / Fail | Pass / Pass | +76.2 | Lc 75: body text | Open in checker |
| Step 5 on background#00627d on #f9fcff | 6.70:1 | 3:1 (WCAG 1.4.11) | Pass / Fail | Pass / Pass | +81.3 | Lc 75: body text | Open in checker |
| Step 6 on background#00566b on #f9fcff | 8.01:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | +86.0 | Lc 75: body text | Open in checker |
| Step 7 on background#004957 on #f9fcff | 9.75:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | +90.8 | Lc 75: body text | Open in checker |
| Step 8 on background#003c43 on #f9fcff | 11.80:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | +95.3 | Lc 75: body text | Open in checker |
| Step 9 on background#002e30 on #f9fcff | 14.22:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | +99.1 | Lc 75: body text | Open in checker |
| Pair | WCAG ratio | Target | Normal AA / AAA | Large AA / AAA | APCA Lc | APCA tier | Check |
|---|---|---|---|---|---|---|---|
| Chart text on background#e8ebef on #141618 | 15.16:1 | AA 4.5:1 | Pass / Pass | Pass / Pass | −93.7 | Lc 75: body text | Open in checker |
| Axis labels on background#aeb1b6 on #141618 | 8.43:1 | AA 4.5:1 | Pass / Pass | Pass / Pass | −59.1 | Lc 45: headlines and large text | Open in checker |
| Axis line on background#717579 on #141618 | 3.90:1 | 3:1 (WCAG 1.4.11) | Fail / Fail | Pass / Fail | −28.5 | Below Lc 30 | Open in checker |
| Step 1 on background#50667d on #141618 | 3.05:1 | 3:1 (WCAG 1.4.11) | Fail / Fail | Pass / Fail | −21.3 | Below Lc 30 | Open in checker |
| Step 2 on background#547793 on #141618 | 3.83:1 | 3:1 (WCAG 1.4.11) | Fail / Fail | Pass / Fail | −28.0 | Below Lc 30 | Open in checker |
| Step 3 on background#5589a9 on #141618 | 4.78:1 | 3:1 (WCAG 1.4.11) | Pass / Fail | Pass / Pass | −35.4 | Lc 30: non-text and incidental | Open in checker |
| Step 4 on background#549cbc on #141618 | 5.92:1 | 3:1 (WCAG 1.4.11) | Pass / Fail | Pass / Pass | −43.5 | Lc 30: non-text and incidental | Open in checker |
| Step 5 on background#56afcd on #141618 | 7.26:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | −52.3 | Lc 45: headlines and large text | Open in checker |
| Step 6 on background#5fc2da on #141618 | 8.82:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | −61.6 | Lc 60: larger content text | Open in checker |
| Step 7 on background#70d4e4 on #141618 | 10.56:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | −71.1 | Lc 60: larger content text | Open in checker |
| Step 8 on background#88e5ec on #141618 | 12.49:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | −81.1 | Lc 75: body text | Open in checker |
| Step 9 on background#a6f5f5 on #141618 | 14.68:1 | 3:1 (WCAG 1.4.11) | Pass / Pass | Pass / Pass | −91.6 | Lc 75: body text | Open in checker |
Colour-blind check
Colours are simulated with the Machado et al. (2009) model at full severity (culori). The figure is the smallest ΔE-OK ×100, the Euclidean distance in OKLab, between any two colours. The build fails if it drops below 3. Still pair colour with labels or patterns (WCAG 1.4.1).
| Vision | Simulated colours | Min ΔE-OK ×100 | Closest pair |
|---|---|---|---|
| Normal vision | 4.3 (≥ 3) | 5 and 6 | |
| Protanopia | 4.4 (≥ 3) | 5 and 6 | |
| Deuteranopia | 3.9 (≥ 3) | 5 and 6 | |
| Tritanopia | 3.6 (≥ 3) | 4 and 5 |
| Vision | Simulated colours | Min ΔE-OK ×100 | Closest pair |
|---|---|---|---|
| Normal vision | 5.3 (≥ 3) | 6 and 7 | |
| Protanopia | 5.3 (≥ 3) | 8 and 9 | |
| Deuteranopia | 4.5 (≥ 3) | 3 and 4 | |
| Tritanopia | 4.9 (≥ 3) | 7 and 8 |
Why these colours
The scale moves from a slate blue to a deeper teal. A small hue change along the way adds a second cue without breaking the lightness order, and chroma swells in the middle where the gamut allows it. The lowest step is found by the same solver used for text: the lightest colour with the scale's hue that still reaches 3:1 against the background.
Steps are evenly spaced in OKLCH lightness, which approximates equal perceived steps, much better than HSL-based ramps do. The accepted minimum difference between neighbouring steps is 3 ΔE-OK under every simulation, so adjacent steps are told apart without being labelled, though a legend is still advised.
The generator solved 24 pairs (12 light, 12 dark), and all of them meet their target on the raw WCAG ratio. The tightest is step 1 on background in the dark theme at 3.05:1 against a target of 3:1.
How to use it
- Bin the data into at most nine classes, and label the class boundaries on the legend.
- Show no-data cells with a hatch or a hollow outline, not a fill from the scale.
- Print the value in the cell when there is room, choosing a label colour that you have tested against that step.
- Do not reverse the scale between charts on the same page; consistent direction avoids misreading.
- For values that diverge around a meaningful midpoint, such as change from zero, use the diverging palette.
Chart advice
- Add a gap or thin background-coloured line between cells so adjacent cells are separate shapes.
- Use a continuous legend bar with tick labels at the class edges.
- Choose cell text colour per step by testing, not by a single rule.
Export
Generated from the verified values above, never typed by hand. Light and dark are both included.
CSS custom properties (hex, then oklch())
/* sRGB hex first: every browser. */
:root {
--color-background: #f9fcff;
--color-text: #1d2022;
--color-text-muted: #55585c;
--color-axis: #83878b;
--color-gridline: #ccd0d5;
--color-step-1: #7c93ac;
--color-step-2: #6286a3;
--color-step-3: #467a99;
--color-step-4: #226e8d;
--color-step-5: #00627d;
--color-step-6: #00566b;
--color-step-7: #004957;
--color-step-8: #003c43;
--color-step-9: #002e30;
}
.dark {
--color-background: #141618;
--color-text: #e8ebef;
--color-text-muted: #aeb1b6;
--color-axis: #717579;
--color-gridline: #33373a;
--color-step-1: #50667d;
--color-step-2: #547793;
--color-step-3: #5589a9;
--color-step-4: #549cbc;
--color-step-5: #56afcd;
--color-step-6: #5fc2da;
--color-step-7: #70d4e4;
--color-step-8: #88e5ec;
--color-step-9: #a6f5f5;
}
/* oklch() where supported: the same colours, unclipped values of the hex above. */
@supports (color: oklch(0 0 0)) {
:root {
--color-background: oklch(0.990 0.005 247.9);
--color-text: oklch(0.241 0.006 236.8);
--color-text-muted: oklch(0.459 0.008 255.5);
--color-axis: oklch(0.621 0.008 247.9);
--color-gridline: oklch(0.856 0.008 253.9);
--color-step-1: oklch(0.654 0.046 250.7);
--color-step-2: oklch(0.604 0.060 243.3);
--color-step-3: oklch(0.556 0.074 236.3);
--color-step-4: oklch(0.507 0.087 229.6);
--color-step-5: oklch(0.4615 0.0874 225.00);
--color-step-6: oklch(0.4199 0.0774 221.72);
--color-step-7: oklch(0.3735 0.0664 216.07);
--color-step-8: oklch(0.326 0.056 207.7);
--color-step-9: oklch(0.274 0.047 199.4);
}
.dark {
--color-background: oklch(0.199 0.005 248.1);
--color-text: oklch(0.939 0.006 255.5);
--color-text-muted: oklch(0.760 0.008 260.7);
--color-axis: oklch(0.560 0.008 248.0);
--color-gridline: oklch(0.334 0.008 240.1);
--color-step-1: oklch(0.502 0.045 249.9);
--color-step-2: oklch(0.554 0.059 243.2);
--color-step-3: oklch(0.606 0.074 236.8);
--color-step-4: oklch(0.6586 0.0864 228.98);
--color-step-5: oklch(0.711 0.095 222.7);
--color-step-6: oklch(0.7636 0.0984 215.86);
--color-step-7: oklch(0.815 0.096 209.0);
--color-step-8: oklch(0.867 0.089 201.9);
--color-step-9: oklch(0.919 0.077 195.9);
}
}Tailwind CSS v4 @theme
@theme {
/* light */
--color-background: oklch(0.990 0.005 247.9); /* #f9fcff */
--color-text: oklch(0.241 0.006 236.8); /* #1d2022 */
--color-text-muted: oklch(0.459 0.008 255.5); /* #55585c */
--color-axis: oklch(0.621 0.008 247.9); /* #83878b */
--color-gridline: oklch(0.856 0.008 253.9); /* #ccd0d5 */
--color-step-1: oklch(0.654 0.046 250.7); /* #7c93ac */
--color-step-2: oklch(0.604 0.060 243.3); /* #6286a3 */
--color-step-3: oklch(0.556 0.074 236.3); /* #467a99 */
--color-step-4: oklch(0.507 0.087 229.6); /* #226e8d */
--color-step-5: oklch(0.4615 0.0874 225.00); /* #00627d */
--color-step-6: oklch(0.4199 0.0774 221.72); /* #00566b */
--color-step-7: oklch(0.3735 0.0664 216.07); /* #004957 */
--color-step-8: oklch(0.326 0.056 207.7); /* #003c43 */
--color-step-9: oklch(0.274 0.047 199.4); /* #002e30 */
/* dark: use as dark:bg-background-dark, or remap under a .dark selector */
--color-background-dark: oklch(0.199 0.005 248.1); /* #141618 */
--color-text-dark: oklch(0.939 0.006 255.5); /* #e8ebef */
--color-text-muted-dark: oklch(0.760 0.008 260.7); /* #aeb1b6 */
--color-axis-dark: oklch(0.560 0.008 248.0); /* #717579 */
--color-gridline-dark: oklch(0.334 0.008 240.1); /* #33373a */
--color-step-1-dark: oklch(0.502 0.045 249.9); /* #50667d */
--color-step-2-dark: oklch(0.554 0.059 243.2); /* #547793 */
--color-step-3-dark: oklch(0.606 0.074 236.8); /* #5589a9 */
--color-step-4-dark: oklch(0.6586 0.0864 228.98); /* #549cbc */
--color-step-5-dark: oklch(0.711 0.095 222.7); /* #56afcd */
--color-step-6-dark: oklch(0.7636 0.0984 215.86); /* #5fc2da */
--color-step-7-dark: oklch(0.815 0.096 209.0); /* #70d4e4 */
--color-step-8-dark: oklch(0.867 0.089 201.9); /* #88e5ec */
--color-step-9-dark: oklch(0.919 0.077 195.9); /* #a6f5f5 */
}shadcn/ui variables (:root and .dark)
:root {
--background: oklch(0.990 0.005 247.9);
--foreground: oklch(0.241 0.006 236.8);
--muted-foreground: oklch(0.459 0.008 255.5);
--border: oklch(0.856 0.008 253.9);
--input: oklch(0.621 0.008 247.9);
--chart-1: oklch(0.654 0.046 250.7);
--chart-2: oklch(0.556 0.074 236.3);
--chart-3: oklch(0.4615 0.0874 225.00);
--chart-4: oklch(0.3735 0.0664 216.07);
--chart-5: oklch(0.274 0.047 199.4);
}
.dark {
--background: oklch(0.199 0.005 248.1);
--foreground: oklch(0.939 0.006 255.5);
--muted-foreground: oklch(0.760 0.008 260.7);
--border: oklch(0.334 0.008 240.1);
--input: oklch(0.560 0.008 248.0);
--chart-1: oklch(0.502 0.045 249.9);
--chart-2: oklch(0.606 0.074 236.8);
--chart-3: oklch(0.711 0.095 222.7);
--chart-4: oklch(0.815 0.096 209.0);
--chart-5: oklch(0.919 0.077 195.9);
}Design tokens (DTCG-style JSON)
{
"color": {
"light": {
"background": {
"$type": "color",
"$value": "oklch(0.990 0.005 247.9)",
"$description": "sRGB #f9fcff"
},
"text": {
"$type": "color",
"$value": "oklch(0.241 0.006 236.8)",
"$description": "sRGB #1d2022"
},
"text-muted": {
"$type": "color",
"$value": "oklch(0.459 0.008 255.5)",
"$description": "sRGB #55585c"
},
"axis": {
"$type": "color",
"$value": "oklch(0.621 0.008 247.9)",
"$description": "sRGB #83878b"
},
"gridline": {
"$type": "color",
"$value": "oklch(0.856 0.008 253.9)",
"$description": "sRGB #ccd0d5"
},
"step-1": {
"$type": "color",
"$value": "oklch(0.654 0.046 250.7)",
"$description": "sRGB #7c93ac"
},
"step-2": {
"$type": "color",
"$value": "oklch(0.604 0.060 243.3)",
"$description": "sRGB #6286a3"
},
"step-3": {
"$type": "color",
"$value": "oklch(0.556 0.074 236.3)",
"$description": "sRGB #467a99"
},
"step-4": {
"$type": "color",
"$value": "oklch(0.507 0.087 229.6)",
"$description": "sRGB #226e8d"
},
"step-5": {
"$type": "color",
"$value": "oklch(0.4615 0.0874 225.00)",
"$description": "sRGB #00627d"
},
"step-6": {
"$type": "color",
"$value": "oklch(0.4199 0.0774 221.72)",
"$description": "sRGB #00566b"
},
"step-7": {
"$type": "color",
"$value": "oklch(0.3735 0.0664 216.07)",
"$description": "sRGB #004957"
},
"step-8": {
"$type": "color",
"$value": "oklch(0.326 0.056 207.7)",
"$description": "sRGB #003c43"
},
"step-9": {
"$type": "color",
"$value": "oklch(0.274 0.047 199.4)",
"$description": "sRGB #002e30"
}
},
"dark": {
"background": {
"$type": "color",
"$value": "oklch(0.199 0.005 248.1)",
"$description": "sRGB #141618"
},
"text": {
"$type": "color",
"$value": "oklch(0.939 0.006 255.5)",
"$description": "sRGB #e8ebef"
},
"text-muted": {
"$type": "color",
"$value": "oklch(0.760 0.008 260.7)",
"$description": "sRGB #aeb1b6"
},
"axis": {
"$type": "color",
"$value": "oklch(0.560 0.008 248.0)",
"$description": "sRGB #717579"
},
"gridline": {
"$type": "color",
"$value": "oklch(0.334 0.008 240.1)",
"$description": "sRGB #33373a"
},
"step-1": {
"$type": "color",
"$value": "oklch(0.502 0.045 249.9)",
"$description": "sRGB #50667d"
},
"step-2": {
"$type": "color",
"$value": "oklch(0.554 0.059 243.2)",
"$description": "sRGB #547793"
},
"step-3": {
"$type": "color",
"$value": "oklch(0.606 0.074 236.8)",
"$description": "sRGB #5589a9"
},
"step-4": {
"$type": "color",
"$value": "oklch(0.6586 0.0864 228.98)",
"$description": "sRGB #549cbc"
},
"step-5": {
"$type": "color",
"$value": "oklch(0.711 0.095 222.7)",
"$description": "sRGB #56afcd"
},
"step-6": {
"$type": "color",
"$value": "oklch(0.7636 0.0984 215.86)",
"$description": "sRGB #5fc2da"
},
"step-7": {
"$type": "color",
"$value": "oklch(0.815 0.096 209.0)",
"$description": "sRGB #70d4e4"
},
"step-8": {
"$type": "color",
"$value": "oklch(0.867 0.089 201.9)",
"$description": "sRGB #88e5ec"
},
"step-9": {
"$type": "color",
"$value": "oklch(0.919 0.077 195.9)",
"$description": "sRGB #a6f5f5"
}
}
}
}Open in the studio
Take the first series colour into the picker, build a tonal scale from it, or check any pair of your own.
Open #7C93AC in the OKLCH picker →Build a tonal scale →Check a pair
FAQ
- Why must every step be 3:1 against the background?
- WCAG 1.4.11 asks for 3:1 for graphical objects needed to understand the content. A heatmap cell is such an object. The cost is a narrower lightness range, which this scale spends on nine steps.
- Is monotonic lightness checked in the tests?
- Yes. The tests check that OKLCH lightness of the final hex values strictly rises (or falls) across the nine steps, in both themes.
- Why reverse direction on dark backgrounds?
- Because on a dark background the high-contrast end is light. Keeping the strongest value most prominent against its background helps reading.
- Can I use fewer steps?
- Yes. Pick evenly spaced steps, such as 1, 3, 5, 7 and 9, which keeps the differences between neighbours larger.