I also did some tests and was not sure if the function light-or-dark is intended to be used on its own because the following output does not seem right:
Here is the file I used to test using sass 1.58 with auto-migrated slash division, defined white and black variables and debug statements at the end:
@use "sass:math";
$white: #fff;
$black: #000;
// @function strip-unit($value)
// @param {number with unit} $value
@function strip-unit($value) {
@return math.div($value, $value * 0 + 1);
}
// @function pow($base, $exponents)
// Helper: Raise a number the power of another number
// Adapted from: https://gist.github.com/hail2u/1964056
//
// @param {number} $base — The base number
// @param {number} $exponents — The exponent to which to raise $base
@function pow($base, $exponents) {
$raised: 1;
@for $i from 1 through $exponents {
$raised: $raised * $base;
}
@return $raised;
}
// @function luminance($color)
// Helper: Calculate Luminance of a single color
// Adapted from: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796
// White luminance is 1, Black luminance is 0
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { luminance: luminance(#c00); }
// Output:
// .sample { luminance: 0.1283736922; }
//
@function luminance($color) {
// Thanks voxpelli for a very concise implementation of luminance measure in sass
// Adapted from: https://gist.github.com/voxpelli/6304812
$rgba: red($color), green($color), blue($color);
$rgba2: ();
@for $i from 1 through 3 {
$rgb: nth($rgba, $i);
$rgb: math.div($rgb, 255);
$rgb: if($rgb < .03928, math.div($rgb, 12.92), pow(math.div($rgb + .055, 1.055), 2));
$rgba2: append($rgba2, $rgb);
}
@return (.2126 * nth($rgba2, 1) + .7152 * nth($rgba2, 2) + 0.0722 * nth($rgba2, 3))*100;
}
// @function contrast-ratio($fg, $bg)
// Helper: Calculate "readability" as defined by WCAG 2.1
// Adapted from: https://github.com/LeaVerou/contrast-ratio/blob/gh-pages/color.js
// Formula: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { color-contrast: contrast-ratio(#c00, #fff); }
// Output:
// .sample { color-contrast: 5.89; }
//
@function contrast-ratio($fg, $bg) {
$luminance1: luminance($fg) + 0.05;
$luminance2: luminance($bg) + 0.05;
$ratio: math.div($luminance1, $luminance2);
@if $luminance2 > $luminance1 {
$ratio: math.div(1, $ratio);
}
// Round to a hundreth because 6.96 should not pass a ratio of 7.0
$ratio: round($ratio * 100) * 0.01;
@return $ratio;
}
// @function validate-font-size($size)
// Helper: Depending on the unit recalculate a font size value into pixels if possible
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { validate-font-size: validate-font-size(1em); }
// Output:
// .sample { validate-font-size: 16; }
//
@function validate-font-size($size) {
@if unit($size) == 'em' or unit($size) == 'rem' or unit($size) == 'px' or unit($size) == '' {
// Check if a flexible unit
@if unit($size) == 'em' or unit($size) == 'rem' {
// Need to convert to a pixel value. Let's not overcomplicate it with possible EM inheritence scale factors
@return strip-unit($size * 16)
}
@if unit($size) == 'px' {
// We expect PX, so strip the value and return it
@return strip-unit($size);
}
@if unit($size) == '' {
@return $size;
}
} @else {
@error 'validate-font-size(): An unexpected font size unit was supplied.';
}
}
// @function get-ratio($level: 'AA', $size: 16, $bold: false)
// Helper: Determine the correct ratio value to use based on font-size and WCAG Level
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { get-ratio: get-ratio('AAA', 19, true); }
// Output:
// .sample { get-ratio: 4.5; }
//
@function get-ratio($level: 'AA', $size: 16, $bold: false) {
// Default ratio
$ratio: 4.5;
@if $level == 'AAA' {
$ratio: 7;
}
// Make sure the size is valid. If the value is not EM, REM, or PX (preferred), we can't help
$size: validate-font-size($size);
// Check font size
@if $size < 24 {
// Small text, use defaults
// But:
@if $size >= 19 and $bold == true {
// Special case: Small text but also bold
@if $level == 'AAA' {
$ratio: 4.5;
} @else {
$ratio: 3;
}
}
} @else {
// Larger than 24
$ratio: 3;
@if $level == 'AAA' {
$ratio: 4.5;
}
}
@return $ratio;
}
// @function light-or-dark($color)
// Helper: Use contrast against white or black to determine if a color is "light" or "dark"
// Adapted from: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796
// To be used in other functions or mixins — creates non-standard CSS output:
// Usage:
// .sample { light-or-dark: light-or-dark(#c00); }
// Output:
// .sample { light-or-dark: "light"; }
//
@function light-or-dark($color) {
$light-contrast: contrast-ratio($color, $white);
$dark-contrast: contrast-ratio($color, $black);
@if $light-contrast > $dark-contrast {
// Contrast against white is higher than against black, so, this is a dark color
@return "dark";
} @else {
@return "light";
}
}
// @function color-contrast($color)
// Helper: Returns black or white compared to a color, whichever produces the highest contrast
// Usage:
// .sample {
// background-color: #c00;
// color: color-contrast(#c00);
// }
// Output:
// .sample {
// background-color: #c00;
// color: #fff;
// }
//
// Named for the forthcoming CSS Module 5 spec function color-contrast();
// See https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-contrast()
//
@function color-contrast($color) {
$color-lod: light-or-dark($color);
@if ($color-lod == "dark") {
@return $white;
} @else {
@return $black;
}
}
// @function a11y-color($fg, $bg, $level: 'AA', $size: 16, $bold: false)
// Goal: Return a color that passes for the chosen WCAG level without changing the Hue of the color
// Usage:
// .sample {
// background-color: #000;
// color: a11y-color(#c0c, #000);
// }
// Output:
// .sample {
// background-color: #000;
// color: #d200d2;
// }
//
@function a11y-color($fg, $bg, $level: 'AA', $size: 16, $bold: false) {
// Helper: make sure the font size value is acceptable
$font-size: validate-font-size($size);
// Helper: With the level, font size, and bold boolean, return the proper target ratio. 3.0, 4.5, or 7.0 expected
$ratio: get-ratio($level, $font-size, $bold);
// Calculate the first contrast ratio of the given pair
$original-contrast: contrast-ratio($fg, $bg);
@if $original-contrast >= $ratio {
// If we pass the ratio already, return the original color
@return $fg;
} @else {
// Doesn't pass. Time to get to work
// Should the color be lightened or darkened?
$fg-lod: light-or-dark($fg);
$bg-lod: light-or-dark($bg);
// Set a "step" value to lighten or darken a color
// Note: Higher percentage steps means faster compile time, but we might overstep the required threshold too far with something higher than 4%
$step: 1%;
// Run through some cases where we want to darken, or use a negative step value
@if $fg-lod == 'light' and $bg-lod == 'light' {
// Both are light colors, darken the fg
$step: - $step;
} @else if $fg-lod == 'dark' and $bg-lod == 'light' {
// bg is light, fg is dark but does not pass, darken more
$step: - $step;
}
// Keeping the rest of the logic here, but our default values do not change, so this logic is not needed
//@else if $fg-lod == 'light' and $bg-lod == 'dark' {
// // bg is dark, fg is light but does not pass, lighten further
// $step: $step;
//} @else if $fg-lod == 'dark' and $bg-lod == 'dark' {
// // Both are dark, so lighten the fg
// $step: $step;
//}
// The magic happens here
// Loop through with a @while statement until the color combination passes our required ratio. Scale the color by our step value until the expression is false
// This might loop 100 times or more depending on the colors
@while contrast-ratio($fg, $bg) < $ratio {
$sat-step: 0%;
@if saturation($fg) > 10 {
$sat-step: $step;
}
$fg: scale-color($fg, $lightness: $step, $saturation:$sat-step);
}
@return $fg;
}
}
@debug (light-or-dark(rgb(30,30,30)));
@debug (light-or-dark(rgb(25, 25, 25)));
@debug (light-or-dark(#1E1E1E));
Hello, really appreciating the effort and very much looking forward to https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/color-contrast.
I noticed two things with the functions:
$white
and$black
) https://github.com/jhogue/automated-a11y-sass/blob/master/a11y-color.scss#L144-L145I also did some tests and was not sure if the function
light-or-dark
is intended to be used on its own because the following output does not seem right:rgb(30, 30, 30)
should not be considered light, as writing with black color on this background is almost invisible (https://webaim.org/resources/contrastchecker/?fcolor=000000&bcolor=1E1E1E).Here is the file I used to test using sass 1.58 with auto-migrated slash division, defined white and black variables and debug statements at the end: