Color Contrast Auditor
Detects color contrast violations that make text unreadable and provides WCAG-compliant fixes. Uses both mathematical contrast ratio analysis and perceptual evaluation via vision capabilities.
When to Use
Activate on:
- Screenshots of websites/apps with suspected contrast issues
- CSS/Tailwind files for color audit
- "I can't read this" or "this is hard to see"
- Pre-launch accessibility checks
- Design system color validation
NOT for:
- Choosing brand colors (use
web-design-expert)
- Color harmony/aesthetics (use
color-theory-palette-harmony-expert)
- Non-visual accessibility (screen readers, keyboard nav)
WCAG 2.1 Contrast Requirements
Minimum Ratios (AA - Required)
| Text Type | Minimum Ratio | Example |
|---|
| Normal text (<24px, <18.66px bold) | 4.5:1 | Body copy, labels, buttons |
| Large text (≥24px or ≥18.66px bold) | 3:1 | Headlines, hero text |
| UI components (borders, icons) | 3:1 | Form inputs, icons, focus rings |
| Graphical objects | 3:1 | Charts, infographics |
Enhanced Ratios (AAA - Recommended)
| Text Type | Minimum Ratio |
|---|
| Normal text | 7:1 |
| Large text | 4.5:1 |
Non-Text Elements
| Element | Requirement |
|---|
| Focus indicators | 3:1 against adjacent colors |
| Form field borders | 3:1 against background |
| Icons conveying meaning | 3:1 against background |
| Disabled elements | No requirement (but consider UX) |
Contrast Ratio Formula
Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
Where L1 = lighter color's relative luminance
L2 = darker color's relative luminance
Calculating Relative Luminance
function relativeLuminance(r, g, b) {
// Convert 0-255 to 0-1
let [rs, gs, bs] = [r, g, b].map(c => c / 255);
// Apply gamma correction
const gamma = c => c <= 0.03928
? c / 12.92
: Math.pow((c + 0.055) / 1.055, 2.4);
const [R, G, B] = [rs, gs, bs].map(gamma);
// Weighted sum (human eye sensitivity)
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
function contrastRatio(color1, color2) {
const l1 = relativeLuminance(...color1);
const l2 = relativeLuminance(...color2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
Common Failing Patterns
1. Light Text on Light Background
❌ FAILING EXAMPLE (from user screenshot):
┌─────────────────────────────────────────────┐
│ Background: #F5F2E8 (beige/cream) │
│ Text: #C8FF00 (lime green) │
│ │
│ "Scan. Crawl. Match. Report." │
│ ← UNREADABLE │
│ │
│ Calculated Ratio: ~1.5:1 │
│ Required: 4.5:1 (normal) or 3:1 (large) │
│ Verdict: FAIL by 3x │
└─────────────────────────────────────────────┘
✅ FIXED OPTIONS:
• Darken text to #5A7300 → Ratio: 4.5:1
• Darken background to #2A2A2A → Ratio: 12:1
• Use dark green #1A4D00 → Ratio: 8:1
2. Gray Text Syndrome
❌ COMMON FAILURE:
Background: #FFFFFF
Text: #AAAAAA (light gray)
Ratio: 2.3:1 ← FAIL
✅ FIXES:
• Text: #767676 → Ratio: 4.5:1 (minimum AA)
• Text: #595959 → Ratio: 7:1 (AAA)
3. Saturated Colors That Look Bright
❌ DECEPTIVE FAILURE:
Background: #FFF8E7 (warm white)
Text: #FF6B6B (coral/salmon)
Ratio: 2.8:1 ← FAIL (looks "colorful" but fails)
✅ FIXES:
• Text: #C62828 (darker red) → Ratio: 5.2:1
• Text: #8B0000 (dark red) → Ratio: 8.1:1
4. Trendy Low-Contrast Aesthetic
❌ "MINIMALIST" FAILURE:
Background: #FAFAFA
Text: #E0E0E0
Ratio: 1.3:1 ← SEVERELY FAILING
This is NOT minimalism. This is inaccessible.
✅ MINIMALIST + ACCESSIBLE:
Background: #FAFAFA
Text: #616161 → Ratio: 5.7:1
5. Placeholder Text Too Light
❌ COMMON FORM FAILURE:
Input background: #FFFFFF
Placeholder: #CCCCCC
Ratio: 1.6:1 ← FAIL
✅ FIX:
Placeholder: #757575 → Ratio: 4.6:1
6. Gradient Backgrounds
❌ VARIABLE CONTRAST:
Gradient: #FFFFFF → #000080
Text: #FFFFFF (fixed)
Top of gradient: 1:1 (invisible!)
Bottom of gradient: 8.6:1 (good)
✅ SOLUTIONS:
• Add text shadow/outline
• Use semi-transparent overlay behind text
• Ensure ALL gradient stops pass contrast
Audit Methodology
Step 1: Visual Scan (Screenshot Analysis)
When given a screenshot, identify:
-
Text elements by size:
- Headlines (large text → 3:1 required)
- Body copy (normal text → 4.5:1 required)
- UI labels (buttons, links → 4.5:1)
- Captions/fine print (4.5:1 required)
-
Interactive elements:
- Button borders/backgrounds
- Form field borders
- Focus states
- Icons with meaning
-
Red flags to look for:
- Light text on light backgrounds
- Gray text on white
- Colored text on colored backgrounds
- Text over images without overlay
Step 2: Extract Colors
From CSS/code:
# Find all color declarations
grep -E "(color:|background:|#[0-9a-fA-F]{3,8}|rgb|hsl)" styles.css
From Tailwind:
# Find text/bg color classes
grep -E "(text-|bg-)" *.tsx *.jsx
Step 3: Calculate Ratios
For each text/background pair:
- Convert colors to RGB
- Calculate relative luminance
- Compute contrast ratio
- Compare to WCAG requirement
Step 4: Generate Report
# Contrast Audit Report
## Summary
- Total color pairs tested: X
- Passing (AA): Y
- Failing: Z
- Critical failures (<2:1): N
## Failures by Severity
### Critical (Ratio < 2:1)
| Location | Foreground | Background | Ratio | Required | Fix |
|----------|------------|------------|-------|----------|-----|
| Hero tagline | #C8FF00 | #F5F2E8 | 1.5:1 | 3:1 | #5A7300 |
### Moderate (Ratio 2:1 - 3:1)
...
### Minor (Ratio 3:1 - 4.5:1, normal text only)
...
## Recommended Fixes
[Specific color replacements with new ratios]
Color Blindness Considerations
Contrast requirements help but don't fully address color blindness. Additional checks:
Types to Consider
| Type | Affected | Consideration |
|---|
| Deuteranopia | 6% of males | Red/green confusion |
| Protanopia | 2% of males | Red appears dark |
| Tritanopia | <1% | Blue/yellow confusion |
Best Practices
-
Never rely on color alone for meaning
- Add icons, patterns, or text labels
- Red/green for error/success needs icons too
-
Test problematic pairs:
- Red + Green (stop/go)
- Blue + Purple
- Green + Brown
- Light green + Yellow
-
Use sufficient lightness difference
- Even with same hue, different lightness helps
Quick Reference: Safe Color Pairs
On White (#FFFFFF)
| Use Case | Color | Hex | Ratio |
|---|
| Body text | Dark gray | #333333 | 12.6:1 |
| Secondary text | Medium gray | #767676 | 4.5:1 |
| Links | Blue | #0066CC | 5.3:1 |
| Success | Green | #2E7D32 | 5.1:1 |
| Error | Red | #C62828 | 6.0:1 |
| Warning | Orange-brown | #E65100 | 4.5:1 |
On Black (#000000)
| Use Case | Color | Hex | Ratio |
|---|
| Body text | Light gray | #E0E0E0 | 13.4:1 |
| Secondary | Medium gray | #9E9E9E | 6.3:1 |
| Accent | Light blue | #90CAF9 | 7.3:1 |
On Dark Gray (#1A1A1A)
| Use Case | Color | Hex | Ratio |
|---|
| Body text | Off-white | #F5F5F5 | 14.1:1 |
| Secondary | Light gray | #BDBDBD | 8.3:1 |
Tools & Validation
Online Checkers
Browser DevTools
// Chrome DevTools: Elements → Styles → hover color swatch
// Shows contrast ratio automatically
// Firefox: Accessibility Inspector
// Shows color contrast issues
Automated Testing
// axe-core (popular a11y testing library)
const axe = require('axe-core');
axe.run(document, { rules: ['color-contrast'] });
// Lighthouse (built into Chrome)
// Performance → Accessibility → Color contrast
CLI Tools
# Lighthouse CLI
npx lighthouse https://example.com --only-categories=accessibility
# Pa11y
npx pa11y https://example.com
Integration with This Skill
When analyzing a screenshot or codebase:
- I will identify all text/background color pairs
- I will calculate contrast ratios for each
- I will flag anything below WCAG AA thresholds
- I will suggest specific hex values that pass
- I will provide before/after comparisons
For the example screenshot (lime on beige):
AUDIT RESULT: CRITICAL FAILURE
Element: Hero tagline "Scan. Crawl. Match. Report."
Foreground: ~#C8FF00 (lime green)
Background: ~#F5F2E8 (beige)
Calculated Ratio: ~1.5:1
Required (large text): 3:1
Required (normal text): 4.5:1
Status: ❌ FAILS BY 2-3x
RECOMMENDED FIXES:
1. Darken text to #5A7300 (olive) → 4.8:1 ✓
2. Darken text to #3D5C00 (dark olive) → 7.1:1 ✓✓
3. Keep lime, darken BG to #3D3D3D → 8.2:1 ✓✓
4. Use #1B5E20 (dark green) → 8.4:1 ✓✓
Checklist for New Designs
Before shipping:
Philosophy: Beautiful design and accessibility are not mutually exclusive. High contrast can be striking, dramatic, and intentional. Low contrast isn't "minimalist"—it's exclusionary. Every unreadable word is a user lost.
Deterministic Audit
Everything above is judgment and reference material for a human-in-the-loop review. scripts/contrast_audit.mjs is the machine-checkable half: given a JSON list of foreground/background pairs (and, optionally, which pieces of meaning are conveyed by color alone), it computes the real WCAG relative-luminance contrast ratio for every pair — never an eyeballed or self-reported one — and fails closed.
node scripts/contrast_audit.mjs --input <contrast-spec>.json
Input matches schemas/contrast-spec.schema.json: a pairs[] array of { name, foreground, background, usage } (hex colors; usage is one of body-text, large-text, ui-component, decorative), plus an optional semanticSignals[] of { name, conveyedByColorOnly }.
Output is { pass, score, findings, recommendations }. Findings:
| id | severity | Fires when |
|---|
contrast-below-threshold | critical | A non-decorative pair's computed ratio is below its required floor (4.5:1 body-text, 3:1 large-text/ui-component). Names the pair and states the real computed ratio, not an estimate. |
invalid-color | critical | A declared foreground/background isn't a parseable #RGB/#RRGGBB hex — fails closed rather than assuming an unreadable value is fine. |
color-only-signal | high | A declared semantic signal (conveyedByColorOnly: true) has no icon/text/pattern backup — WCAG 1.4.1, independent of any contrast ratio. |
decorative pairs are exempt from the ratio check (but still validated for parseable hex). pass = !hasCritical && score >= 75; a single contrast-below-threshold or invalid-color finding always fails the audit regardless of score.
See examples/sample-input.json (all pairs pass) and examples/expected-output.md (a #777777-on-#FFFFFF body-text pair that looks fine but computes to 4.48:1 — just under the 4.5:1 floor — audited, fixed, and re-audited to pass:true).
Output Contract
A completed contrast audit carries:
pass / score: whether every non-decorative pair clears its WCAG floor and no signal relies on color alone.
findings: one entry per violation, each naming the specific pair or signal, its computed ratio (for contrast findings), and the required threshold.
recommendations: a concrete fix per finding — a specific alternative hex from references/safe-color-pairs.md, or the non-color indicator to add.
- A written report (see
templates/output-template.md) that a designer or reviewer can act on without re-deriving the math.
Anti-Patterns
Eyeballing a "Close Enough" Gray
Novice: Sees #777777 body text on white, judges it "basically mid-gray, should be fine," and ships without computing the ratio.
Expert: Always computes the real WCAG relative-luminance ratio before judging a pair — #777777 on #FFFFFF is 4.48:1, just under the 4.5:1 body-text floor, close enough to fool the eye but not close enough to pass.
Detection: contrast_audit.mjs returns contrast-below-threshold (critical) whenever a body-text, large-text, or ui-component pair's computed ratio is below its required threshold, quoting the exact computed value.
Trendy Low-Contrast Aesthetic
Novice: Ships #E0E0E0 text on #FAFAFA background and calls it "minimalist," treating the near-invisibility as an intentional design choice.
Expert: Low contrast isn't minimalism, it's exclusion — enforces the same WCAG floor on a "quiet" design as on a loud one, using references/safe-color-pairs.md for pre-verified alternatives that keep the restrained palette.
Detection: Same contrast-below-threshold finding; the scorer does not special-case a pair because it was labeled intentional or on-brand.
Color as the Only Signal
Novice: Marks a form field's error state with a red border and nothing else, or a status dot that's "green means good, red means bad" with no label or icon.
Expert: Declares every such signal explicitly and backs it with a non-color indicator (icon, text, pattern) so meaning survives color blindness and grayscale rendering — contrast alone doesn't fix a color-only-signal defect.
Detection: contrast_audit.mjs returns color-only-signal (high) whenever a semanticSignals[] entry has conveyedByColorOnly: true, independent of whether the color itself has good contrast.
References
| File | Load When |
|---|
references/safe-color-pairs.md | Need a pre-verified, ready-to-use color combination instead of guessing a fix by hand. |
schemas/contrast-spec.schema.json | Need to validate a contrast-spec JSON payload's structure before auditing it. |
examples/sample-input.json | Need a minimal all-passing spec to see the expected input shape. |
examples/expected-output.md | Need to see a spec with a subtle near-miss ratio, an invalid hex, and a color-only signal audited, then fixed and re-audited to pass:true. |
templates/output-template.md | Need a reusable audit-report template to fill in for a design review. |
scripts/contrast_audit.mjs | Need deterministic, real-math scoring of a set of color pairs. |
agents/openai.yaml | Need a subagent descriptor for delegated contrast auditing. |
Layout QA gate (mechanical — run before shipping)
Before calling any rendered page, artifact, dashboard, deck, or component done,
run the mechanical overflow/collision checker. It renders the page headlessly and
flags text-vs-text collisions, clipped/ellipsis-truncated elements, text escaping
its container, and horizontal page scroll — the visual defects a screenshot hides
and that only appear at a specific width or in one theme.
Resolve layout-overflow-guard from the active skill catalog before running it.
The command below shows the standard Claude install path; use the path reported
by your harness. If the skill is absent, install or sync it instead of skipping
this gate.
python3 ~/.claude/skills/layout-overflow-guard/scripts/check_layout.py <file-or-url> \
--widths 1280,1100,860,720,390 --themes light,dark
You do not need to read check_layout.py — invoke it with the Bash tool and
act on its report and exit code (non-zero = a defect). The script's source never
enters your context; only its findings do. Drive it to zero violations across
every width and both themes before you ship. Full detail: the
layout-overflow-guard skill.
<!-- BEGIN BUNDLE INDEX (auto: index_references.py) -->
Skill Bundle Index
Every file in this skill, and when to open it. Auto-generated; run scripts/index_references.py --fix.
root
CHANGELOG.md — Color Contrast Auditor — Changelog — - Imported from the global jury_rig skill catalog into the repo.
README.md — Color Contrast Auditor — Detects color contrast violations that make text and UI unreadable, and provides WCAG-compliant fixes backed by a real relative-luminance ca
agents/
examples/
examples/expected-output.md — Example Output: Color Contrast Auditor — Scenario: a design brief eyeballs #777777 gray body text over a white card because it "reads fine on the design file" — the classic gray-t
examples/sample-input.json — sample input (data/schema)
references/
references/safe-color-pairs.md — Pre-Calculated Safe Color Pairs — Ready-to-use color combinations that pass WCAG AA (4.5:1 for normal text, 3:1 for large text).
schemas/
scripts/
templates/
<!-- END BUNDLE INDEX -->