Skip to content

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:

StandardNormal TextLarge Text
WCAG AA4.5:13:1
WCAG AAA7:14.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:

php
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

php
$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:

php
/**
 * 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:

php
// 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

php
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

php
// 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:

ColorHexWCAG AA (4.5:1)
Black#000000Pass
Dark gray#333333Pass
Medium gray#767676Pass (minimum)
Light gray#777777Fail
Navy blue#000080Pass
Standard blue#0066CCPass
Bright green#00FF00Fail
Orange#FF6600Fail
Yellow#FFFF00Fail

Testing Your Implementation

php
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.