Accessible Color Palette for Charts: WCAG Contrast, Alt Text, and Data Tables

⏱️ 11 min read 📊 Visualization

An accessible color palette for charts has to do more than hold up for color-blind readers. Under WCAG 2.x, every mark a reader needs (bars, lines, markers, slices) must have at least 3:1 contrast against the colors next to it, all chart text needs 4.5:1, and color can never be the only way to tell series apart. The palette is only part of the job: the chart also needs a text alternative, and an interactive chart has to work from the keyboard.

Quick answer: To meet WCAG 2.1 or 2.2 Level AA, give every bar, line, marker, and pie slice at least 3:1 contrast against its background and any adjacent colors (SC 1.4.11). Give axis labels, legends, data labels, and tooltip text at least 4.5:1 (SC 1.4.3; 3:1 for text 24px and up, or about 18.5px bold). Add direct labels, markers, or patterns so color is never the only cue (SC 1.4.1). Then give the chart a short alt text that states the takeaway, plus a longer description, ideally a real HTML data table (SC 1.1.1). Colorblind-safe palettes do not pass automatically: the Okabe-Ito orange, sky blue, and yellow are all below 3:1 on white.

What makes an accessible color palette for charts?

It has to pass two different tests. "Colorblind-safe" means the hues stay distinguishable for people with color vision deficiencies. WCAG contrast is about luminance: the lightness difference between a color and whatever sits next to it, which is what people with low vision rely on. A palette can pass one test and fail the other. WCAG has no rule written specifically for charts; the requirements come from several success criteria (SC):

Success criterionLevelWhat it covers in a chartRequirement
1.1.1 Non-text ContentAThe chart as a wholeA text alternative that serves the same purpose: short alt text plus a long description for complex charts
1.4.1 Use of ColorALegends, series, highlighted valuesColor is not the only visual means of conveying information or distinguishing elements
1.4.3 Contrast (Minimum)AATitles, axis and tick labels, legends, data labels, annotations, tooltips4.5:1; 3:1 for large text (18pt/24px, or 14pt bold, about 18.5px)
1.4.11 Non-text ContrastAA (added in 2.1)Bars, lines, markers, slices, gridlines needed to read values, focus indicators3:1 against adjacent colors
2.1.1 KeyboardATooltips, filters, drill-downs, series togglesEverything a mouse can do, the keyboard can do
1.4.13 Content on Hover or FocusAA (added in 2.1)Custom tooltipsDismissible, hoverable, and persistent

Level AA is the level most accessibility policies point to, so 3:1 and 4.5:1 are the numbers that matter in practice. They are hard thresholds: W3C says computed ratios should not be rounded, so 4.499:1 does not meet 4.5:1, and 2.99:1 does not meet 3:1.

What contrast ratio do chart bars, lines, and markers need?

At least 3:1 against adjacent colors, under SC 1.4.11 Non-text Contrast, for any part of a graphic that is required to understand the content. What counts as "adjacent" depends on the chart type. W3C's Understanding 1.4.11 document (informative guidance, not the normative rule) works through line graphs and pie charts, and the same logic extends to other chart types:

There are escape hatches. If visible text conveys the same information, such as a value label on every bar or slice, the marks are no longer required for understanding, but that text then has to meet 4.5:1. The same applies when the information is available in another form, such as a data table next to the chart. W3C also lists color gradients that represent a measurement, such as heat maps, under the criterion's "essential" exception. Treat these as fallbacks, not targets: a low-vision reader still looks at the chart first. And while WCAG sets no minimum line width, anti-aliasing can render a hairline lighter than its specified color, so 2px or heavier lines hold their contrast better.

Is a colorblind-safe palette WCAG compliant?

Not automatically. The Okabe-Ito palette is one of the most widely used colorblind-safe categorical palettes, yet three of its eight colors fall below 3:1 against white, so as bars or lines on a white background they fail SC 1.4.11. On a near-black dark theme the result flips: every chromatic color passes and only black fails. The ratios below are computed with the WCAG relative-luminance formula and rounded to two decimals:

Okabe-Ito colorHexOn white (#FFFFFF)On dark (#121212)
Orange#E69F002.25:1 (fail)8.32:1 (pass)
Sky blue#56B4E92.31:1 (fail)8.12:1 (pass)
Bluish green#009E733.42:1 (pass)5.48:1 (pass)
Yellow#F0E4421.32:1 (fail)14.17:1 (pass)
Blue#0072B25.19:1 (pass)3.61:1 (pass)
Vermillion#D55E003.87:1 (pass)4.84:1 (pass)
Reddish purple#CC79A73.06:1 (pass)6.12:1 (pass)
Black#00000021.00:1 (pass)1.12:1 (fail)

That does not make Okabe-Ito a bad choice. It means a palette has to be checked against the background it will actually sit on, in every theme you ship. For a four-series chart on white, use the members that pass: blue, vermillion, bluish green, and reddish purple. For how hues hold up under deuteranopia, protanopia, and tritanopia, see designing charts for color blindness.

How do I make a stacked bar chart pass WCAG contrast?

Start with those four passing colors on white, then fix the places where they touch each other and where text sits on or near them. All four clear 3:1 against white, but against each other they range from only 1.12:1 (bluish green vs. reddish purple) to 1.69:1 (blue vs. reddish purple), so touching segments fail SC 1.4.11. Four changes fix the chart:

  1. Separate the segments. Add a white gap between them, for example a 2px white stroke on each rect. Each segment is now adjacent to white, which all four clear at 3.06:1 or better.
  2. Add a second cue besides color. The four colors are similar in lightness, which is exactly why they barely contrast with each other, so label segments directly or add patterns. That satisfies SC 1.4.1.
  3. Set value labels in a dark neutral, not the series color. Vermillion text on white is 3.87:1, below the 4.5:1 text minimum; #333333 is 12.63:1.
  4. Check labels inside bars against the fill. White on blue #0072B2 is 5.19:1 (pass). White on vermillion #D55E00 is 3.87:1 (fail); black on vermillion is 5.43:1 (pass).

How do I check a chart palette's contrast?

Use any WCAG contrast checker (WebAIM's is the one most people know), or compute the ratio in your build so a palette change cannot silently regress. The formula is WCAG's own: linearize each sRGB channel, weight the channels into relative luminance, then divide (lighter + 0.05) by (darker + 0.05). Compare the unrounded result against the threshold.

def luminance(hex_color):
    r, g, b = (int(hex_color.lstrip("#")[i:i+2], 16) / 255 for i in (0, 2, 4))
    lin = lambda c: c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
    return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)

def contrast(a, b):
    hi, lo = sorted((luminance(a), luminance(b)), reverse=True)
    return (hi + 0.05) / (lo + 0.05)

background = "#FFFFFF"
palette = {"blue": "#0072B2", "vermillion": "#D55E00",
           "orange": "#E69F00", "sky blue": "#56B4E9"}

for name, color in palette.items():
    ratio = contrast(color, background)
    marks = "pass" if ratio >= 3 else "FAIL"
    text = "pass" if ratio >= 4.5 else "FAIL"
    print(f"{name:<11}{ratio:5.2f}:1  marks {marks}  text {text}")

# blue        5.19:1  marks pass  text pass
# vermillion  3.87:1  marks pass  text FAIL
# orange      2.25:1  marks FAIL  text FAIL
# sky blue    2.31:1  marks FAIL  text FAIL

Run the loop once per background you ship (page, card, dark theme) and once for each pair of colors that touch, such as adjacent stack segments.

What contrast do chart labels and axis text need?

At least 4.5:1 under SC 1.4.3, for all informative text: chart titles, axis titles, tick labels, legend entries, data labels, annotations, and tooltip text. Large text needs only 3:1, but WCAG defines large as 18pt (24px) or 14pt bold (about 18.5px), which most tick labels are not. The catch is that contrast is measured against the background actually behind the text:

Does WCAG allow color-coded charts?

Yes. SC 1.4.1 Use of Color does not ban color; it bans color as the only visual means of conveying information or distinguishing elements. A line chart whose only link between each line and its legend entry is hue fails, even if every color passes 3:1. Add a second channel:

For choosing the hues in the first place, see choosing a color palette (sequential vs. diverging vs. categorical) and color best practices.

How do I write alt text for a chart?

Write it in two parts, the approach W3C's WAI images tutorial describes for complex images: a short alt text that identifies the chart and gives its main point, and a long description that carries the detail. The short part should name the chart type, the subject and time frame, and the takeaway in a sentence or two. Skip "image of," because screen readers already announce an image or graphic, and never use alt="" on an informative chart, since empty alt tells assistive technology the image is decorative.

Alt textVerdict
"Chart"Names the element, not the content
"Image of a line graph"Redundant "image of," and says nothing about the data
"Line chart of monthly signups by plan, January to June 2025"Identifies the chart but leaves out the point
"Line chart of monthly signups by plan, January to June 2025: Pro rose from 420 to 780 while Free stayed flat near 1,500. Data table below."Good: type, subject, takeaway, and where to find the details

The long description then gives the scales, values, and trends. For the same chart:

The chart plots Free and Pro signups for January through June 2025 on a y-axis from 0 to 1,800. Free signups stay between 1,480 and 1,540 every month. Pro signups rise every month, from 420 in January to 780 in June, an increase of 360 (86%). By June, Pro is about half of Free (780 vs. 1,505).

Put the long description where everyone can reach it: visible text or a figure caption below the chart, a collapsible section, or a link to a separate page. Avoid two older habits: the HTML longdesc attribute is obsolete, and aria-describedby reads its target as one flat string, so it cannot carry a table's rows and columns.

Should an accessible chart include a data table?

Usually, yes. A real HTML table is the most complete long description: screen reader users can move cell by cell and hear the row and column headers, keyboard and low-vision users get exact values, and W3C's Understanding 1.4.11 notes that when the information is available in another form, such as a table, the graphic itself is no longer required for understanding. It also gives every reader exact numbers, which a chart can only approximate (see table vs. chart).

<figure>
  <img src="signups.png"
       alt="Line chart of monthly signups by plan, January to June 2025:
            Pro rose from 420 to 780 while Free stayed flat near 1,500.
            Data table below.">
  <figcaption>
    Pro signups grew 86% in six months; Free was flat.
    <details>
      <summary>Show the data for this chart</summary>
      <table>
        <caption>Monthly signups by plan, January–June 2025</caption>
        <thead>
          <tr><th scope="col">Month</th><th scope="col">Free</th><th scope="col">Pro</th></tr>
        </thead>
        <tbody>
          <tr><th scope="row">Jan</th><td>1,510</td><td>420</td></tr>
          <tr><th scope="row">Feb</th><td>1,480</td><td>465</td></tr>
          <tr><th scope="row">Mar</th><td>1,525</td><td>530</td></tr>
          <tr><th scope="row">Apr</th><td>1,495</td><td>610</td></tr>
          <tr><th scope="row">May</th><td>1,540</td><td>690</td></tr>
          <tr><th scope="row">Jun</th><td>1,505</td><td>780</td></tr>
        </tbody>
      </table>
    </details>
  </figcaption>
</figure>

Use a <caption>, <th> cells with scope, and real table markup. A grid built from divs loses the row and column relationships that screen readers announce (SC 1.3.1 Info and Relationships). The <details> element keeps the table one click away without a script, and it works from the keyboard out of the box.

How do I add ARIA to an SVG chart?

For a static SVG chart, expose it as a single image: put role="img" on the <svg>, make a <title> its first child, add a <desc>, and point aria-labelledby at both. The img role makes the SVG's contents presentational, so screen readers skip the dozens of tick labels and path fragments inside; the name, the description, and the adjacent table carry the content.

<svg viewBox="0 0 640 320" role="img"
     aria-labelledby="signups-title signups-desc">
  <title id="signups-title">Line chart: monthly signups by plan, January–June 2025</title>
  <desc id="signups-desc">Pro rose from 420 to 780 while Free stayed flat
    near 1,500. Full data in the table below the chart.</desc>
  <!-- axes, gridlines, lines, labels -->
</svg>

For <canvas> charts, which many JavaScript charting libraries draw into, there is nothing inside to expose: the drawing is pixels. Add role="img" and an aria-label to the canvas element, and put the long description and data table in the page next to it.

How do I make an interactive chart keyboard accessible?

Anything a mouse user can do with the chart, a keyboard user must be able to do too (SC 2.1.1, Level A): reveal values in tooltips, click to filter, drill down, toggle series. Hover-only tooltips are the most common failure. The practical checklist:

<svg viewBox="0 0 640 320" role="group" aria-labelledby="signups-title">
  <title id="signups-title">Monthly signups by plan, January–June 2025</title>
  <g aria-hidden="true"><!-- axes, gridlines, tick labels --></g>

  <!-- roving tabindex: only the current point is in the Tab order;
       arrow keys move focus and swap the tabindex values -->
  <circle class="point" cx="600" cy="100" r="6" fill="#0072B2"
          tabindex="0" role="button"
          aria-label="Pro, June 2025: 780 signups. Show June details."></circle>
  <circle class="point" cx="490" cy="120" r="6" fill="#0072B2"
          tabindex="-1" role="button"
          aria-label="Pro, May 2025: 690 signups. Show May details."></circle>
</svg>

<style>
  .point:focus-visible {
    outline: none;
    stroke: #1A1A1A;   /* 17.40:1 on white, 3.36:1 against the #0072B2 fill */
    stroke-width: 3;
  }
</style>

Common mistakes

Most chart accessibility failures come from checking the wrong thing, not from skipping the check entirely:

Pro Tip: Give marks and text separate color jobs. Let the series colors do the 3:1 work on bars and lines, and set every label, tick, and legend entry in one dark neutral (#333333 is 12.63:1 on white) next to a color swatch or at the end of its line. Swapping palettes then cannot break your label contrast (SC 1.4.3); only the 3:1 mark checks need rerunning, once per theme background and once per pair of touching colors.

← Back to Visualization Tips