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.

Light theme: 12 pairs
PairWCAG ratioTargetNormal AA / AAALarge AA / AAAAPCA LcAPCA tierCheck
Chart text on background#1d2022 on #f9fcff15.91:1AA 4.5:1Pass / PassPass / Pass+101.3Lc 75: body textOpen in checker
Axis labels on background#55585c on #f9fcff6.94:1AA 4.5:1Pass / FailPass / Pass+82.8Lc 75: body textOpen in checker
Axis line on background#83878b on #f9fcff3.51:13:1 (WCAG 1.4.11)Fail / FailPass / Fail+61.8Lc 60: larger content textOpen in checker
Step 1 on background#7c93ac on #f9fcff3.07:13:1 (WCAG 1.4.11)Fail / FailPass / Fail+56.8Lc 45: headlines and large textOpen in checker
Step 2 on background#6286a3 on #f9fcff3.73:13:1 (WCAG 1.4.11)Fail / FailPass / Fail+63.9Lc 60: larger content textOpen in checker
Step 3 on background#467a99 on #f9fcff4.52:13:1 (WCAG 1.4.11)Pass / FailPass / Pass+70.2Lc 60: larger content textOpen in checker
Step 4 on background#226e8d on #f9fcff5.53:13:1 (WCAG 1.4.11)Pass / FailPass / Pass+76.2Lc 75: body textOpen in checker
Step 5 on background#00627d on #f9fcff6.70:13:1 (WCAG 1.4.11)Pass / FailPass / Pass+81.3Lc 75: body textOpen in checker
Step 6 on background#00566b on #f9fcff8.01:13:1 (WCAG 1.4.11)Pass / PassPass / Pass+86.0Lc 75: body textOpen in checker
Step 7 on background#004957 on #f9fcff9.75:13:1 (WCAG 1.4.11)Pass / PassPass / Pass+90.8Lc 75: body textOpen in checker
Step 8 on background#003c43 on #f9fcff11.80:13:1 (WCAG 1.4.11)Pass / PassPass / Pass+95.3Lc 75: body textOpen in checker
Step 9 on background#002e30 on #f9fcff14.22:13:1 (WCAG 1.4.11)Pass / PassPass / Pass+99.1Lc 75: body textOpen in checker
Dark theme: 12 pairs
PairWCAG ratioTargetNormal AA / AAALarge AA / AAAAPCA LcAPCA tierCheck
Chart text on background#e8ebef on #14161815.16:1AA 4.5:1Pass / PassPass / Pass−93.7Lc 75: body textOpen in checker
Axis labels on background#aeb1b6 on #1416188.43:1AA 4.5:1Pass / PassPass / Pass−59.1Lc 45: headlines and large textOpen in checker
Axis line on background#717579 on #1416183.90:13:1 (WCAG 1.4.11)Fail / FailPass / Fail−28.5Below Lc 30Open in checker
Step 1 on background#50667d on #1416183.05:13:1 (WCAG 1.4.11)Fail / FailPass / Fail−21.3Below Lc 30Open in checker
Step 2 on background#547793 on #1416183.83:13:1 (WCAG 1.4.11)Fail / FailPass / Fail−28.0Below Lc 30Open in checker
Step 3 on background#5589a9 on #1416184.78:13:1 (WCAG 1.4.11)Pass / FailPass / Pass−35.4Lc 30: non-text and incidentalOpen in checker
Step 4 on background#549cbc on #1416185.92:13:1 (WCAG 1.4.11)Pass / FailPass / Pass−43.5Lc 30: non-text and incidentalOpen in checker
Step 5 on background#56afcd on #1416187.26:13:1 (WCAG 1.4.11)Pass / PassPass / Pass−52.3Lc 45: headlines and large textOpen in checker
Step 6 on background#5fc2da on #1416188.82:13:1 (WCAG 1.4.11)Pass / PassPass / Pass−61.6Lc 60: larger content textOpen in checker
Step 7 on background#70d4e4 on #14161810.56:13:1 (WCAG 1.4.11)Pass / PassPass / Pass−71.1Lc 60: larger content textOpen in checker
Step 8 on background#88e5ec on #14161812.49:13:1 (WCAG 1.4.11)Pass / PassPass / Pass−81.1Lc 75: body textOpen in checker
Step 9 on background#a6f5f5 on #14161814.68:13:1 (WCAG 1.4.11)Pass / PassPass / Pass−91.6Lc 75: body textOpen 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).

Light chart theme
VisionSimulated coloursMin ΔE-OK ×100Closest pair
Normal vision4.3 (≥ 3)5 and 6
Protanopia4.4 (≥ 3)5 and 6
Deuteranopia3.9 (≥ 3)5 and 6
Tritanopia3.6 (≥ 3)4 and 5
Dark chart theme
VisionSimulated coloursMin ΔE-OK ×100Closest pair
Normal vision5.3 (≥ 3)6 and 7
Protanopia5.3 (≥ 3)8 and 9
Deuteranopia4.5 (≥ 3)3 and 4
Tritanopia4.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.

← All accessible palettes