Validación de Contraste WCAG en PHP: Construyendo Utilidades de Colores Accesibles
Introducción
El contraste de colores es uno de los problemas de accesibilidad más comunes en la web. Cuando los usuarios pueden elegir colores personalizados para sus perfiles, temas o contenido, necesitas validar que sus elecciones sigan siendo legibles. Este artículo muestra cómo implementar dos utilidades PHP que resuelven este problema: una para verificaciones rápidas de brillo y otra para validación completa de conformidad WCAG.
Entendiendo los Requisitos de Contraste WCAG
Las Pautas de Accesibilidad para el Contenido Web (WCAG) definen ratios mínimos de contraste para texto:
| Estándar | Texto Normal | Texto Grande |
|---|---|---|
| WCAG AA | 4.5:1 | 3:1 |
| WCAG AAA | 7:1 | 4.5:1 |
El texto grande se define como 18pt (24px) o 14pt (18.66px) en negrita.
TIP
Aunque 4.5:1 cumple con los requisitos, es el mínimo, no el ideal. Muchos usuarios todavía tienen dificultades con el contraste mínimo. Cuando sea posible, apunta a valores más altos.
Verificando si un Color es Claro u Oscuro
Para casos de uso simples—como decidir si usar texto blanco o negro sobre un fondo de color—puedes usar una verificación rápida de luminancia basada en la fórmula W3C:
public static function isLightColor(string $hex): bool
{
// Elimina # si está presente
$hex = ltrim($hex, '#');
// Convierte hex de 3 dígitos a 6 dígitos
if (strlen($hex) === 3) {
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
}
// Convierte hex a RGB
$r = hexdec(substr($hex, 0, 2));
$g = hexdec(substr($hex, 2, 2));
$b = hexdec(substr($hex, 4, 2));
// Calcula luminancia usando fórmula W3C
$luminance = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255;
// Retorna true si el color es claro
return $luminance > 0.5;
}La fórmula da más peso al verde (0.587) porque los ojos humanos son más sensibles a la luz verde.
Ejemplo de Uso
$colorDeFondo = '#3498db';
if (isLightColor($colorDeFondo)) {
$colorDeTexto = '#000000'; // Usa texto negro en fondos claros
} else {
$colorDeTexto = '#ffffff'; // Usa texto blanco en fondos oscuros
}Validación Completa de Contraste WCAG
Para cumplir adecuadamente con la accesibilidad, necesitas el cálculo completo de luminancia relativa WCAG. Esta función valida si un color tiene suficiente contraste contra un fondo blanco:
/**
* Valida si un color hex tiene suficiente contraste con fondo blanco
*
* @param string $hexColor El color hex a validar (con o sin prefijo #)
* @param float $minContrastRatio El ratio mínimo de contraste (4.5 para WCAG AA)
* @return bool True si el color tiene suficiente contraste
*/
public static function hasGoodContrastWithWhite(
string $hexColor,
float $minContrastRatio = 4.5
): bool {
// Convierte hex a RGB
$hex = ltrim($hexColor, '#');
// Maneja hex de 3 dígitos
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));
// Calcula luminancia relativa (según WCAG)
$r = $r / 255;
$g = $g / 255;
$b = $b / 255;
// Lineariza valores sRGB
$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);
// Calcula luminancia con coeficientes WCAG
$luminance = 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;
// Calcula ratio de contraste contra blanco (luminancia = 1)
$contrastRatio = ($luminance + 0.05) / (1 + 0.05);
// WCAG AA requiere al menos 4.5:1 para texto normal
return (1 / $contrastRatio) >= $minContrastRatio;
}Importante
Las dos fórmulas de luminancia parecen similares pero sirven propósitos diferentes:
- Fórmula W3C (0.299, 0.587, 0.114): Verificación rápida de brillo percibido
- Fórmula WCAG (0.2126, 0.7152, 0.0722): Conformidad precisa de accesibilidad con linearización sRGB
Entendiendo la Linearización sRGB
El cálculo WCAG incluye un paso crítico que la fórmula simple omite: linearización sRGB. Las pantallas no muestran colores de forma lineal—aplican corrección gamma. El umbral de 0.03928 y las fórmulas de transformación convierten de valores de pantalla a intensidad real de luz:
// Para valores bajos (colores oscuros)
$valorLineal = $valorSrgb / 12.92;
// Para valores más altos
$valorLineal = pow(($valorSrgb + 0.055) / 1.055, 2.4);Esta linearización es la razón por la que #777777 pasa WCAG AA mientras que #787878 puede fallar, aunque parezcan casi idénticos al ojo humano.
Ejemplos Prácticos de Uso
Validando Colores Elegidos por el Usuario
public function updateUserTheme(Request $request): Response
{
$colorPrimario = $request->input('primary_color');
if (!hasGoodContrastWithWhite($colorPrimario)) {
return response()->json([
'error' => 'Este color no tiene suficiente contraste para texto. Prueba un tono más oscuro.'
], 422);
}
// Guarda el color validado
$user->update(['primary_color' => $colorPrimario]);
return response()->json(['success' => true]);
}Soportando Diferentes Niveles WCAG
// WCAG AA para texto normal (por defecto)
$pasaAA = hasGoodContrastWithWhite($color);
// WCAG AA para texto grande (14pt negrita o 18pt)
$pasaAAGrande = hasGoodContrastWithWhite($color, 3.0);
// WCAG AAA para texto normal
$pasaAAA = hasGoodContrastWithWhite($color, 7.0);
// WCAG AAA para texto grande
$pasaAAAGrande = hasGoodContrastWithWhite($color, 4.5);Referencia Rápida: Colores Comunes
Así se comportan los colores comunes contra fondos blancos:
| Color | Hex | WCAG AA (4.5:1) |
|---|---|---|
| Negro | #000000 | Pasa |
| Gris oscuro | #333333 | Pasa |
| Gris medio | #767676 | Pasa (mínimo) |
| Gris claro | #777777 | Falla |
| Azul marino | #000080 | Pasa |
| Azul estándar | #0066CC | Pasa |
| Verde brillante | #00FF00 | Falla |
| Naranja | #FF6600 | Falla |
| Amarillo | #FFFF00 | Falla |
Probando Tu Implementación
describe('hasGoodContrastWithWhite', function () {
it('pasa para colores oscuros', function () {
expect(hasGoodContrastWithWhite('#000000'))->toBeTrue();
expect(hasGoodContrastWithWhite('#333333'))->toBeTrue();
expect(hasGoodContrastWithWhite('#0066CC'))->toBeTrue();
});
it('falla para colores claros', function () {
expect(hasGoodContrastWithWhite('#FFFFFF'))->toBeFalse();
expect(hasGoodContrastWithWhite('#EEEEEE'))->toBeFalse();
expect(hasGoodContrastWithWhite('#FFFF00'))->toBeFalse();
});
it('maneja notación hex de 3 dígitos', function () {
expect(hasGoodContrastWithWhite('#000'))->toBeTrue();
expect(hasGoodContrastWithWhite('#fff'))->toBeFalse();
});
it('respeta ratios de contraste personalizados', function () {
$grisMedio = '#888888';
expect(hasGoodContrastWithWhite($grisMedio, 4.5))->toBeFalse();
expect(hasGoodContrastWithWhite($grisMedio, 3.0))->toBeTrue();
});
});Conclusión
Construir aplicaciones accesibles requiere validar las elecciones de colores programáticamente. La función isLightColor ofrece una forma rápida de elegir colores de texto contrastantes, mientras que hasGoodContrastWithWhite garantiza conformidad total con WCAG para colores elegidos por usuarios.
Recuerda: la conformidad con accesibilidad es un requisito legal en muchas jurisdicciones. A partir de abril de 2026, los sitios web de gobiernos estatales y locales de EE.UU. deben cumplir los estándares WCAG 2.1 AA, y regulaciones similares existen en la UE y otras regiones.

