WCAG Contrast Validation in PHP: Building Accessible Color Utilities
Introduction
Color contrast is one of the most common accessibility issues on the web. When users can choose custom colors for their profiles, themes, or content, you need to validate that their choices remain readable. This article shows you how to implement two PHP utilities that solve this problem: one for quick brightness checks, and another for full WCAG compliance validation.
Understanding WCAG Contrast Requirements
The Web Content Accessibility Guidelines (WCAG) define minimum contrast ratios for text:
| Standard | Normal Text | Large Text |
|---|---|---|
| WCAG AA | 4.5:1 | 3:1 |
| WCAG AAA | 7:1 | 4.5:1 |
Large text is defined as 18pt (24px) or 14pt (18.66px) bold.
TIP
While 4.5:1 meets compliance, it's the floor, not the ceiling. Many users still struggle with minimum-passing contrast. When possible, aim higher.
Checking if a Color is Light or Dark
For simple use cases—like deciding whether to use white or black text on a colored background—you can use a quick luminance check based on the W3C formula:
public static function isLightColor(string $hex): bool
{
// Remove # if present
$hex = ltrim($hex, '#');
// Convert 3-digit hex to 6-digit
if (strlen($hex) === 3) {
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
}
// Convert hex to RGB
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
// Calculate luminance using W3C formula
$luminance = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255;
// Return true if color is light
return $luminance > 0.5;
}The formula weights green most heavily (0.587) because human eyes are most sensitive to green light.
Usage Example
$backgroundColor = '#3498db';
if (isLightColor($backgroundColor)) {
$textColor = '#000000'; // Use black text on light backgrounds
} else {
$textColor = '#ffffff'; // Use white text on dark backgrounds
}Full WCAG Contrast Validation
For proper accessibility compliance, you need the full WCAG relative luminance calculation. This function validates whether a color has sufficient contrast against a white background:
/**
* Validates if a hex color has sufficient contrast with white background
*
* @param string $hexColor The hex color to validate (with or without # prefix)
* @param float $minContrastRatio The minimum contrast ratio (4.5 for WCAG AA)
* @return bool True if the color has sufficient contrast
*/
public static function hasGoodContrastWithWhite(
string $hexColor,
float $minContrastRatio = 4.5
): bool {
// Convert hex to RGB
$hex = ltrim($hexColor, '#');
// Handle 3-digit hex
if (strlen($hex) === 3) {
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
}
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
// Calculate relative luminance (per WCAG)
$r = $r / 255;
$g = $g / 255;
$b = $b / 255;
// Linearize sRGB values
$r = $r <= 0.03928 ? $r / 12.92 : pow(($r + 0.055) / 1.055, 2.4);
$g = $g <= 0.03928 ? $g / 12.92 : pow(($g + 0.055) / 1.055, 2.4);
$b = $b <= 0.03928 ? $b / 12.92 : pow(($b + 0.055) / 1.055, 2.4);
// Calculate luminance with WCAG coefficients
$luminance = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
// Calculate contrast ratio against white (luminance = 1)
$contrastRatio = ($luminance + 0.05) / (1 + 0.05);
// WCAG AA requires at least 4.5:1 for normal text
return (1 / $contrastRatio) >= $minContrastRatio;
}Important
The two luminance formulas look similar but serve different purposes:
- W3C formula (0.299, 0.587, 0.114): Quick perceived brightness check
- WCAG formula (0.2126, 0.7152, 0.0722): Precise accessibility compliance with sRGB linearization
Understanding the sRGB Linearization
The WCAG calculation includes a critical step that the simple formula skips: sRGB linearization. Computer displays don't show colors linearly—they apply gamma correction. The threshold of 0.03928 and the transformation formulas convert from display values to actual light intensity:
// For low values (dark colors)
$linearValue = $srgbValue / 12.92;
// For higher values
$linearValue = pow(($srgbValue + 0.055) / 1.055, 2.4);This linearization is why #777777 passes WCAG AA while #787878 might fail, even though they look nearly identical to the human eye.
Practical Usage Examples
Validating User-Chosen Colors
public function updateUserTheme(Request $request): Response
{
$primaryColor = $request->input('primary_color');
if (!hasGoodContrastWithWhite($primaryColor)) {
return response()->json([
'error' => 'This color doesn\'t have enough contrast for text. Try a darker shade.'
], 422);
}
// Save the validated color
$user->update(['primary_color' => $primaryColor]);
return response()->json(['success' => true]);
}Supporting Different WCAG Levels
// WCAG AA for normal text (default)
$passesAA = hasGoodContrastWithWhite($color);
// WCAG AA for large text (14pt bold or 18pt)
$passesAALarge = hasGoodContrastWithWhite($color, 3.0);
// WCAG AAA for normal text
$passesAAA = hasGoodContrastWithWhite($color, 7.0);
// WCAG AAA for large text
$passesAAALarge = hasGoodContrastWithWhite($color, 4.5);Quick Reference: Common Colors
Here's how common colors perform against white backgrounds:
| Color | Hex | WCAG AA (4.5:1) |
|---|---|---|
| Black | #000000 | Pass |
| Dark gray | #333333 | Pass |
| Medium gray | #767676 | Pass (minimum) |
| Light gray | #777777 | Fail |
| Navy blue | #000080 | Pass |
| Standard blue | #0066CC | Pass |
| Bright green | #00FF00 | Fail |
| Orange | #FF6600 | Fail |
| Yellow | #FFFF00 | Fail |
Testing Your Implementation
describe('hasGoodContrastWithWhite', function () {
it('passes for dark colors', function () {
expect(hasGoodContrastWithWhite('#000000'))->toBeTrue();
expect(hasGoodContrastWithWhite('#333333'))->toBeTrue();
expect(hasGoodContrastWithWhite('#0066CC'))->toBeTrue();
});
it('fails for light colors', function () {
expect(hasGoodContrastWithWhite('#FFFFFF'))->toBeFalse();
expect(hasGoodContrastWithWhite('#EEEEEE'))->toBeFalse();
expect(hasGoodContrastWithWhite('#FFFF00'))->toBeFalse();
});
it('handles 3-digit hex notation', function () {
expect(hasGoodContrastWithWhite('#000'))->toBeTrue();
expect(hasGoodContrastWithWhite('#fff'))->toBeFalse();
});
it('respects custom contrast ratios', function () {
$mediumGray = '#888888';
expect(hasGoodContrastWithWhite($mediumGray, 4.5))->toBeFalse();
expect(hasGoodContrastWithWhite($mediumGray, 3.0))->toBeTrue();
});
});Conclusion
Building accessible applications requires validating color choices programmatically. The isLightColor function gives you a quick way to pick contrasting text colors, while hasGoodContrastWithWhite ensures full WCAG compliance for user-generated color choices.
Remember: accessibility compliance is a legal requirement in many jurisdictions. As of April 2026, US state and local government websites must meet WCAG 2.1 AA standards, and similar regulations exist in the EU and other regions.

