Color Contrast Audit for Tailwind Palettes: Systematically Checking WCAG-Compliant Colors
AI generated
</>
tw
Tailwind CSS · Accessibility · Color Contrast · WCAG
Color Contrast Audit for Tailwind Palettes
systematically checking WCAG-compliant colors

A beautiful color palette is not automatically a readable color palette. A color contrast audit reveals which text and background combinations of a Tailwind CSS palette actually pass WCAG AA, before they become a trap for users with reduced vision in the design system.

13 min read Contrast ratio · WCAG AA/AAA · dark mode · CI audit Tailwind CSS v3 · v4 · WCAG 2.1/2.2

1. Why color contrast decides readability

A color contrast audit starts with an uncomfortable truth: most design teams choose colors by aesthetic feel, not by a measurable contrast ratio. A light gray on white looks pleasantly subtle on the designer's monitor, but for users with low vision, in direct sunlight, or on a poorly calibrated display, the same text can become practically unreadable. This is exactly where a systematic color contrast audit comes in: it replaces subjective feeling with a calculable metric.

Tailwind CSS already ships a wide range of color steps from 50 to 950 in its default palette, but not every combination of text color and background color within that palette automatically achieves sufficient contrast. A color contrast audit systematically checks which combinations actually work, making visible where a design system silently builds barriers for millions of users with visual impairments.

2. WCAG contrast requirements: AA, AAA and large text

The Web Content Accessibility Guidelines define a minimum contrast ratio of 4.5 to 1 for normal body text at conformance level AA in success criterion 1.4.3. For large text, defined as at least 18 point or 14 point bold, a reduced ratio of 3 to 1 is sufficient, because larger characters remain recognizable even with lower contrast. Level AAA tightens these values to 7 to 1 for normal text and 4.5 to 1 for large text, a level many commercial products aim for but rarely fully reach.

A color contrast audit must consider these different thresholds separately depending on text size and font weight. A combination that is sufficient for a 24 point heading can already be insufficient for the same color tone in 14 point body text. Anyone running an audit with only a single blanket threshold regularly overlooks cases where small text with an otherwise acceptable color still violates WCAG.

3. The contrast problem in Tailwind's default palette

Tailwind's default palette is deliberately broad, with eleven steps per color family from 50 to 950. That also means neighboring steps like slate-400 and slate-500 deliver completely different contrast values on a white background, even though they differ only slightly visually. A common mistake in projects is using text-slate-400 for secondary text on a white background, a combination with a contrast ratio of about 2.9 to 1, clearly below the WCAG AA threshold of 4.5 to 1.

A color contrast audit of the default palette shows that reliable WCAG AA on a white background is only reached from slate-500 onward, or for most color families from the 600 or 700 step. This insight can be turned into a design system rule: secondary text should never fall below a certain color step, regardless of how good it looks visually in the mockup.


/* Common contrast trap: slate-400 on white fails WCAG AA for normal text */
.secondary-text-wrong {
  color: #94a3b8; /* slate-400, contrast ratio ~2.9:1 on white */
}

/* slate-600 reliably reaches WCAG AA (4.5:1) for normal-sized body text */
.secondary-text-correct {
  color: #475569; /* slate-600, contrast ratio ~7.2:1 on white */
}

/* slate-500 only clears the reduced 3:1 threshold for large/bold text */
.large-text-only {
  color: #64748b; /* slate-500, contrast ratio ~4.6:1 on white, borderline */
  font-size: 1.5rem;
  font-weight: 700;
}

4. Calculating contrast ratios: the formula behind it

A contrast ratio is based on the relative luminance of both colors, not on a simple brightness comparison of RGB values. Relative luminance weights red, green and blue differently, because the human eye is most sensitive to green and least sensitive to blue. From the two relative luminance values, the contrast ratio is calculated as the ratio of the lighter to the darker luminance, each with a small offset of 0.05 added to avoid division by zero.

For a color contrast audit, this formula does not need to be recalculated by hand, it is enough to implement it once as a function and then run it programmatically across the entire Tailwind palette. That is exactly what enables a systematic, repeatable audit, instead of manually checking individual color combinations in an online tool.


// Contrast ratio calculation following the WCAG 2.x formula
function relativeLuminance(hex) {
  const rgb = hexToRgb(hex).map((channel) => {
    const c = channel / 255;
    return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
}

function contrastRatio(hexA, hexB) {
  const lumA = relativeLuminance(hexA);
  const lumB = relativeLuminance(hexB);
  const lighter = Math.max(lumA, lumB);
  const darker = Math.min(lumA, lumB);
  return (lighter + 0.05) / (darker + 0.05);
}

function hexToRgb(hex) {
  const clean = hex.replace('#', '');
  return [0, 2, 4].map((i) => parseInt(clean.substring(i, i + 2), 16));
}

console.log(contrastRatio('#475569', '#ffffff').toFixed(2)); // 7.24 -> passes AA and AAA
console.log(contrastRatio('#94a3b8', '#ffffff').toFixed(2)); // 2.86 -> fails AA

5. Systematically auditing Tailwind color scales

A complete color contrast audit does not just test a single combination, but every relevant text color against every relevant background color that actually appears in the project. The practical approach: import the Tailwind configuration as a JavaScript object, extract all color steps, and check every combination against the WCAG AA threshold in a nested loop. The result is a matrix that shows at a glance which combinations are safe, which are borderline, and which are clearly non-compliant.

Such an audit becomes especially valuable when it tests not only white and black as backgrounds, but also the backgrounds actually used in the project, such as slate-50 for cards or slate-900 for dark sections. Text that passes WCAG AA on pure white can already fall just short on a slightly tinted card background, a detail that pure theory without a project specific color contrast audit easily misses.


// Audit every text/background combination actually used in the project
import resolveConfig from 'tailwindcss/resolveConfig';
import tailwindConfig from './tailwind.config.js';

const config = resolveConfig(tailwindConfig);
const usedTextColors = ['slate-400', 'slate-500', 'slate-600', 'slate-700'];
const usedBackgrounds = ['white', 'slate-50', 'slate-900'];

const results = [];
for (const textKey of usedTextColors) {
  for (const bgKey of usedBackgrounds) {
    const textHex = resolveColor(config, textKey);
    const bgHex = resolveColor(config, bgKey);
    const ratio = contrastRatio(textHex, bgHex);
    results.push({
      text: textKey,
      background: bgKey,
      ratio: ratio.toFixed(2),
      passesAA: ratio >= 4.5,
    });
  }
}

console.table(results);

6. Automation with scripts and CI pipelines

A manual color contrast audit is valuable for an initial inventory, but quickly loses effectiveness once new components regularly introduce new color combinations. The sustainable solution is integrating the audit script into the CI pipeline, so every pull request is checked automatically before a new, low contrast combination even reaches the main branch. A simple non-zero exit code for at least one failed combination is enough to fail the build.

Additionally, such a script can be combined with axe-core, which checks actually rendered elements in a browser context rather than just comparing static color values from the configuration. This combination of a static color contrast audit at the configuration level and a dynamic test on rendered pages covers both systematic design system errors and individual cases where a component sets inline styles with problematic contrast contrary to the configuration.


#!/usr/bin/env bash
# CI step: fail the build if any color combination drops below WCAG AA
set -euo pipefail

echo "Running Tailwind contrast audit..."
node scripts/contrast-audit.js --config tailwind.config.js --threshold 4.5

if [ $? -ne 0 ]; then
  echo "[ERROR] Contrast audit failed: at least one combination is below WCAG AA"
  exit 1
fi

echo "[OK] All audited color combinations pass WCAG AA"

7. Designing contrast-safe custom palettes in Tailwind v4

Anyone defining their own brand colors in Tailwind CSS v4 via the @theme directive leaves the well tested default palette behind and takes on full responsibility for sufficient contrast themselves. The recommended workflow: for every new brand color, immediately calculate a matching dark text variant and a matching light background variant, instead of noticing contrast problems only after the fact in the finished interface. A color contrast audit right when new tokens are created prevents problematic colors from ever being built into components in the first place.

In practice this means defining at least two contrast checked text variants for every brand color, one for light and one for dark backgrounds, and naming these as their own design tokens instead of scattering raw hex values across components. This keeps the color contrast audit a one time effort per token, not a recurring effort per component.


/* Tailwind v4: brand color tokens with pre-audited contrast-safe text variants */
@import "tailwindcss";

@theme {
  --color-brand: oklch(0.55 0.18 250);        /* base brand color */
  --color-brand-text-on-light: oklch(0.32 0.15 250); /* audited: 7.1:1 on white */
  --color-brand-text-on-dark: oklch(0.85 0.08 250);  /* audited: 8.4:1 on slate-900 */
}

.brand-heading-light-bg {
  color: var(--color-brand-text-on-light);
}

.brand-heading-dark-bg {
  color: var(--color-brand-text-on-dark);
}

8. Dark mode contrast: double checking required

A common misconception in dark mode implementations: if a color combination passes WCAG AA in light mode, it is often assumed that the inverted combination automatically passes in dark mode too. That is almost never true, because contrast ratios do not invert linearly, and because many design systems in dark mode do not simply swap colors, but use their own, more muted tones to avoid harsh, fatiguing contrasts on a dark background.

A complete color contrast audit must therefore run both modes separately, with its own text and background pairs for light and dark mode. Tailwind's dark: modifier makes this technically easy to implement, but does not replace the content check of whether dark:text-slate-300 on dark:bg-slate-900 actually meets the same contrast requirement as its light counterpart. Anyone who audits only light mode in practice often leaves out half of the users, especially since dark mode is now actively chosen by a significant share of users.

9. Color contrast tools compared

Different tools are available for a color contrast audit, differing in automatability, accuracy, and integration into existing Tailwind workflows.

Tool Automatable Tailwind integration Typical use
WebAIM Contrast Checker No, manual None, hex values entered individually Spot checking individual colors
Custom Node script Yes, fully Reads tailwind.config.js directly Full palette audit, CI integration
axe-core Yes, in browser context Tests rendered elements, not configuration End to end tests on real pages
Figma contrast plugins Partially Only in the design tool, not in code Early check before implementation
Lighthouse Yes, can integrate into CI Tests the rendered page as a whole Rough overview, no palette details

In practice these tools complement each other best in combination: a custom Node script for the complete, repeatable color contrast audit of the configuration, axe-core for spot check end to end tests on real pages, and a Figma plugin as an early warning before a problematic color even reaches the code.

Mironsoft

Tailwind CSS, accessibility and WCAG-compliant frontend development

Colors that stay readable on every screen?

We run a full color contrast audit of your Tailwind palette, for both light and dark mode, and integrate an automated check script into your CI pipeline, so low contrast colors never make it into production.

Palette Audit

Systematic review of every text and background combination

CI Integration

Automated contrast script that stops the build on violations

Dark Mode Check

Separate contrast validation for light and dark mode tokens

10. Summary

A systematic color contrast audit replaces subjective design feeling with a measurable, repeatable check against the WCAG standard. The formula behind the contrast ratio is based on relative luminance, not simple brightness, and can be implemented once to then run programmatically across the entire Tailwind palette. Tailwind's default palette offers many color steps, but not every one of them achieves sufficient contrast on every background.

Integrating a color contrast audit into the CI pipeline permanently prevents new components from unnoticeably introducing low contrast color combinations. Separate checking of light and dark mode remains especially important, since contrast ratios do not automatically carry over between the two modes. Custom palettes in Tailwind CSS v4 should be designed from the start with contrast checked text tokens, instead of discovering contrast problems only after the fact in the finished interface.

Color Contrast Audit for Tailwind Palettes - the essentials at a glance

WCAG AA minimum value

4.5 to 1 for normal text, 3 to 1 for large text from 18 point or 14 point bold.

Check contrasts programmatically

Implement the formula once, then run it across the entire tailwind.config.js palette.

Check light and dark mode separately

Contrast ratios do not automatically carry over between the two modes.

Integrate into CI pipeline

Run the audit script in the build so low contrast colors never reach production.

11. FAQ: Color Contrast Audit for Tailwind Palettes

1What is a color contrast audit?
Systematic check whether text and background combinations meet WCAG minimum contrast, instead of deciding subjectively.
2Contrast value for WCAG AA?
4.5 to 1 for normal text, 3 to 1 for large text from 18 point or 14 point bold.
3Does the default palette pass WCAG AA?
Not automatically. slate-400 on white sits below 4.5 to 1, reliable compliance starts around slate-600.
4How is the ratio calculated?
Via relative luminance of both colors following the WCAG formula, ratio of lighter to darker luminance plus 0.05.
5Can this be automated?
Yes, a Node script reads the Tailwind configuration and checks every combination programmatically, ideal for CI.
6Does dark mode need separate checks?
Yes, contrast values do not automatically carry over between light and dark mode.
7Custom brand colors in v4?
Define contrast checked text tokens for light and dark backgrounds immediately, not after the fact.
8Is axe-core enough alone?
No, it only checks rendered elements. It complements a systematic palette audit but does not replace it.
9Difference between AA and AAA?
AA requires 4.5 to 1, AAA tightens to 7 to 1 for normal text, with lower values for large text.
10Which backgrounds to include?
All backgrounds actually used, not just white and black, also tinted card backgrounds and dark sections.