// @function strip-unit($value) // @param {number with unit} $value @function strip-unit($value) { @return ($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: calc($rgb / 255); $rgb: if($rgb < .03928, calc($rgb / 12.92), pow(calc(($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: calc($luminance1 / $luminance2); @if $luminance2 > $luminance1 { $ratio: calc(1 / $ratio); } // Round to a hundreth because 6.96 should not pass a ratio of 7.0 $ratio: calc(round($ratio * 100) / 100); @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; } }