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.
Table of Contents
- 1. Why color contrast decides readability
- 2. WCAG contrast requirements: AA, AAA and large text
- 3. The contrast problem in Tailwind's default palette
- 4. Calculating contrast ratios: the formula behind it
- 5. Systematically auditing Tailwind color scales
- 6. Automation with scripts and CI pipelines
- 7. Designing contrast-safe custom palettes in Tailwind v4
- 8. Dark mode contrast: double checking required
- 9. Color contrast tools compared
- 10. Summary
- 11. FAQ
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.