Last week, I was redesigning my portfolio site and found myself squinting at color combinations, wondering if they'd pass accessibility standards. I knew the WCAG guidelines existed, but I kept forgetting the exact thresholds. "Is 4.5:1 the AA standard for normal text or large text?" I'd ask myself, then immediately check the same documentation for the fifth time that month.
The problem wasn't finding a contrast checker—there are dozens. The problem was that most online tools either required uploading screenshots, were cluttered with ads, or didn't let me quickly test multiple color pairs without refreshing. I wanted something I could use offline, without sending my design colors to some server, and that would give me immediate feedback as I tweaked values.
So, like any reasonable developer, I decided to build my own. Because apparently I enjoy reinventing wheels.
The Math Behind WCAG Contrast
Before diving into code, let's talk about what actually goes into calculating contrast ratio. The WCAG 2.1 specification defines a specific formula that accounts for how humans perceive brightness. It's not just about comparing hex values—it's about relative luminance.
The calculation breaks down into three steps:
- Convert each RGB channel to a linear value using the sRGB transfer function
- Calculate relative luminance using weighted channels (red is weighted less than green because our eyes are more sensitive to green)
- Compute the contrast ratio by comparing the lighter and darker colors
Here's the core function that does the heavy lifting:
function contrastRatio(fg, bg) {
const L1 = relLuminance(fg);
const L2 = relLuminance(bg);
return (Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05);
}
The relLuminance function applies the sRGB gamma correction:
function relLuminance({r, g, b}) {
const [R, G, B] = [r, g, b].map(v => {
const c = v / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
That 0.03928 threshold in the formula always trips me up. It's the point where the sRGB curve transitions from linear to exponential behavior. I initially forgot this and just applied the power function to everything—the results were close but wrong enough to matter for borderline cases.
Building the Tool with AI Assistance
Now here's where the process got interesting. I decided to build this as a small browser-based utility, and I wanted to use AI assistance to speed things up. I described the requirements to Claude, specifying the WCAG 2.1 formula and the exact thresholds I needed.
The first iteration was... surprisingly good. The AI generated the core calculation functions correctly on the first try. It even handled edge cases like short hex codes (#RGB format) and invalid inputs. I was impressed.
But then came the UI. The AI's initial design was functional but ugly—a single column layout that required scrolling to see results. I had to iterate with specific prompts:
"Make the color inputs side-by-side, add a live preview area, and show the AA/AAA results in a table format."
What the AI got wrong: it initially used Math.round() for the contrast ratio, which caused issues with the threshold comparisons. A 4.499999 ratio would display as "4.5" and show as passing AA when it shouldn't. Classic floating-point precision problem.
I had to explicitly prompt: "Don't round the ratio before comparing against thresholds. Keep full precision for the comparison, round only for display."
This is a lesson I've learned repeatedly with AI coding: it's great at generating boilerplate and standard patterns, but it doesn't understand the domain-specific edge cases. You have to know enough to catch those mistakes.
The "Swap Colors" Feature
One feature I really wanted was a quick swap button. When you're testing text colors, you often want to flip foreground and background to see which combination works better. This turned out to be deceptively simple:
function swapColors() {
const fgHex = document.getElementById('fgHex').value;
document.getElementById('fgHex').value = document.getElementById('bgHex').value;
document.getElementById('bgHex').value = fgHex;
updateFromHex('fg');
updateFromHex('bg');
}
The tricky part wasn't the swap itself—it was making sure the color picker inputs stayed in sync. The native <input type="color"> element only accepts hex values, so I needed bidirectional synchronization between the hex text inputs and the color pickers.
The AI handled this synchronization logic well, but I had to step in when it tried to use onchange events instead of oninput. For a tool like this, you want real-time updates as the user types or drags the color picker, not updates only when they click away.
Handling Invalid Inputs
One edge case that required careful thought: what happens when someone types garbage into the hex input? The naive approach is to just ignore it, but that creates a confusing experience where the user types "not a color" and nothing happens.
I decided to show an error message but keep the last valid color. This way, the contrast ratio stays visible while the user corrects their input. It's a small UX decision, but it makes the tool feel responsive rather than broken.
function parseHex(hex) {
const cleaned = hex.replace('#', '');
if (cleaned.length === 3) {
return {
r: parseInt(cleaned[0] + cleaned[0], 16),
g: parseInt(cleaned[1] + cleaned[1], 16),
b: parseInt(cleaned[2] + cleaned[2], 16)
};
}
if (cleaned.length === 6) {
return {
r: parseInt(cleaned.slice(0, 2), 16),
g: parseInt(cleaned.slice(2, 4), 16),
b: parseInt(cleaned.slice(4, 6), 16)
};
}
return null;
}
This function handles the three-digit shorthand format, which many developers forget about. The AI initially only supported six-digit hex, but I caught that when testing with colors like #fff.
Why Not Use an Existing Library?
You might be wondering why I didn't just use a library like color-contrast-checker from npm. Fair question. The answer is:
- Bundle size: For a single-page utility, adding a whole library for one formula is overkill
- Offline capability: I wanted zero dependencies so the tool works from a local file
- Learning: Sometimes you need to understand the math yourself to debug visual issues
That said, if I were building this into a larger application, I'd absolutely use a well-tested library. The formula has enough edge cases (like handling alpha channels, which I'm deliberately ignoring) that reinventing it in production code is risky.
The Dark Mode Trap
Here's something that bit me: I added dark mode support using CSS variables, but initially forgot to update the contrast checker's own UI colors. So the tool itself had text that failed WCAG AA in dark mode. The irony was palpable—an accessibility tool that was itself inaccessible.
The fix was straightforward once I noticed it, but it's a reminder that accessibility isn't just about the tools you build; it's about everything you build with those tools.
Performance Considerations
Since this is pure frontend with no network requests, performance is mostly about avoiding unnecessary re-renders. The contrast calculation itself is trivial—even a 10-year-old phone can do thousands of these calculations per second.
The real performance concern is the color picker. Native <input type="color"> elements can fire oninput events dozens of times per second while the user drags the color wheel. If you're doing expensive DOM manipulation on each event, you'll get lag.
My solution was simple: keep the calculation and DOM updates in a single function that only touches the necessary elements. No virtual DOM, no framework, just vanilla JavaScript with direct DOM manipulation. For a tool this small, frameworks add complexity without adding value.
What AI Got Right (and Wrong)
Let me be honest about the AI-assisted development experience:
AI handled well:
- The core WCAG formula implementation
- CSS styling and responsive layout
- The table structure for displaying AA/AAA results
- Initial event handling structure
AI got wrong:
- Floating-point precision in threshold comparisons
- Only supporting six-digit hex codes initially
- Using
onchangeinstead ofoninputfor real-time updates - Forgetting to handle the
#prefix inconsistency (users might type "fff" without the hash)
The pattern I've noticed: AI excels at generating code that matches common patterns and best practices, but struggles with domain-specific edge cases. You need to know enough about the problem domain to catch these issues.
Lessons Learned
Always test with known values: I used the WCAG example from their documentation (white on black should be 21:1) to verify my implementation. Don't trust your code until it produces known-correct outputs.
Floating-point math is sneaky: When comparing ratios against thresholds, always use the full precision value. Rounding for display is fine, but never round for logic.
AI is a great pair programmer, not a replacement: The AI wrote maybe 70% of this tool, but I spent most of my time reviewing, testing, and fixing edge cases. It saved me time on boilerplate but didn't eliminate the need for domain knowledge.
Accessibility tools should be accessible: It's easy to forget that the tool itself needs to meet the standards it's checking.
The Result
During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file with embedded CSS and JavaScript—no dependencies, no build step, works offline. You can find it at craftvo.app/en/tool/color-contrast if you want to try it out.
The tool lets you pick foreground and background colors, shows the contrast ratio in real-time, and displays a table with all the WCAG AA/AAA pass/fail criteria. It handles both normal and large text thresholds, because that distinction trips up more people than you'd think.
Final Thoughts
Building this tool reinforced something I already suspected: the WCAG contrast formula is simple enough to implement from scratch, but subtle enough that you need to test carefully. The AI-assisted development process saved me time on the boring parts, but the real value came from understanding the domain well enough to catch the AI's mistakes.
Whether you're building an accessibility tool or just checking your own designs, knowing how contrast ratios work makes you a better developer. And if you're using AI to write code, remember: it's a tool, not a replacement for understanding. The best results come from combining AI's speed with your own domain knowledge.
Now if you'll excuse me, I need to go fix the contrast ratio on my own portfolio site. Turns out that "modern design" trend of gray text on white backgrounds? Yeah, that fails WCAG AA. Who knew?
Top comments (0)