The only valid color contrast function in PHP and SCSS
Published: – Leave a comment
Yes, it’s me again with the same topic again. After I already mentioned to fix your SCSS color contrast function and how to use a WCAG 2 compliant SCSS color contrast function with scssphp
, a colleague noticed that in some cases, this function was wrong.
Unfortunately, it’s not easy to find a correct implementation to be WCAG 2 compliant and get the same result as the WebAIM contrast checker. In the end, it took a while to found the php-contrast library from @breadthe, which does the correct math.
It is very similar to the functions I showed. However, instead of just comparing the difference of two brightnesses in absolute integers, it compares them differently, which actually returns the correct contrast ratio.
In PHP, you can use it like this:
/**
* Calculate the contrast between two colors and return either a light or dark
* contrast color.
*
* @param string $hex_color The color to test
* @param string $dark The dark color
* @param string $light The light color
* @return string The dark or the light color
*/
function get_color_contrast_color( string $hex_color, string $dark = '#000', string $light = '#fff' ): string {
if ( empty( $hex_color ) ) {
return $light;
}
$background_luminance = \get_luminance( $hex_color );
$color_luminance = \get_luminance( $dark );
$contrast_ratio = ( \max( $color_luminance, $background_luminance ) + .05 ) / ( \min( $color_luminance, $background_luminance ) + .05 );
// if contrast is more than 4.5, return black color
if ( $contrast_ratio >= 4.5 ) {
return $dark;
}
return $light;
}
/**
* Calculate luminance of a color.
*
* @see https://github.com/breadthe/php-contrast/blob/master/src/HexColorPair.php#L92-L112
*
* @param string $color Hex color with 6 or 8 digits
* @return float Color luminance
*/
function get_luminance( string $color ): float {
// Get decimal values
$red = \base_convert( \substr( $color, 1, 2 ), 16, 10 );
$green = \base_convert( \substr( $color, 3, 2 ), 16, 10 );
$blue = \base_convert( \substr( $color, 5, 2 ), 16, 10 );
// Get sRGB values
$red_srgb = $red / 255;
$green_srgb = $green / 255;
$blue_srgb = $blue / 255;
// Calculate luminance
$r = ( $red_srgb <= .03928 ) ? $red_srgb / 12.92 : \pow( ( ( $red_srgb + .055 ) / 1.055 ), 2.4 );
$g = ( $green_srgb <= .03928 ) ? $green_srgb / 12.92 : \pow( ( ( $green_srgb + .055 ) / 1.055 ), 2.4 );
$b = ( $blue_srgb <= .03928 ) ? $blue_srgb / 12.92 : \pow( ( ( $blue_srgb + .055 ) / 1.055 ), 2.4 );
return .2126 * $r + .7152 * $g + .0722 * $b;
}
Code language: PHP (php)
The get_luminance
helper function is basically the same as already used in the two mentioned articles above. In the function get_color_contrast_color
I use the luminance of the given color and compare it with the dark color. If its contrast ratio is higher than 4.5, it return the dark color, as the contrast ratio is sufficient to be WCAG 2 compliant. Otherwise, it returns the light color.
In SCSS, you can use it like this:
@use "sass:math";
@function de-gamma($n) {
@if $n <= 0.03928 {
@return math.div($n, 12.92);
}
@else {
@return math.pow((math.div(($n + .055), 1.055)), 2.4);
}
}
@function luminance($c) {
$rlin: de-gamma(math.div(red($c), 255));
$glin: de-gamma(math.div(green($c), 255));
$blin: de-gamma(math.div(blue($c), 255));
@return 0.2126 * $rlin + 0.7152 * $glin + 0.0722 * $blin;
}
@function contrast-color($color, $dark: #000, $light: #fff) {
@if $color == null {
@return null;
}
@else {
$color-luminance: luminance($color);
$dark-luminance: luminance($dark);
@if (math.div(math.max($color-luminance, $dark-luminance) + .05, math.min($color-luminance, $dark-luminance) + .05) >= 4.5) {
@return $light;
}
@else {
@return $dark;
}
}
}
Code language: SCSS (scss)
Please keep in mind that this function does not always return a color with sufficient contrast ratio, depending on the given color and the given dark and light colors. It always returns the dark color if the contrast ratio is above a specific threshold and the light color otherwise.