Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <h1>Sass Color Contrast Functions</h1>
<p><a href="https://codepen.io/giana/project/full/ZWbGzD" target="_parent">Read the documentation</a>. Note: Colors are randomized, so this page is going to be ugly. Run it in edit mode for new colors or set your own.</p>

<section>
  <div class="color-block">
    <h2>Settings</h2>
    <div class="color color1">#1</div>
    <div class="color color2">#2</div>
    <br />
    <code>Ratio: <span class="ratio"></span> </code>
    <code>Balance: <span class="balance"></span></code>
  </div>
</section>

<section>
<div class="color-block fix-color">
  <h2>fix-color()</h2>
  
  <div class="color">Fix #1</div>
  <div class="color">Fix #2</div>
</div>

<div class="color-block fix-contrast">
  <h2>fix-contrast()</h2>
  
  <div class="color">Fix both</div>
  <div class="color">Fix both</div>
</div>

<div class="color-block best-contrast">
  <h2>best-contrast()</h2>
  
  <div class="color">#3</div>
  <div class="color">Fix #3</div>
</div>
</section>

<section>
<div class="color-block scale-luminance">
  <h2>scale-luminance()</h2>
  
  <div class="color">Match #1<br>with #2</div>
</div>
  
<div class="color-block check-contrast">
  <h2>check-contrast()</h2>
  
  <code class="result"></code>
</div>

<div class="color-block luminance">
  <h2>luminance()</h2>
  
  <code class="result">#1, #2</code>
</div>
  
</section>

<footer><p>Note: Pseudo elements are being used to generate text for this demo. This isn't their purpose, and it can be an <a href="http://tink.uk/accessibility-support-for-css-generated-content/">accessibility concern</a>.</p></footer>
              
            
!

CSS

              
                /*
  WCAG color contrast formula
  https://www.w3.org/TR/2016/NOTE-WCAG20-TECHS-20161007/G18#G18-procedure

  This pen uses the non-standard Sass pow() function
  https://css-tricks.com/snippets/sass/power-function/
  Using it outside of CodePen requires you provide your own pow() function with support for decimals

  To generate random colors, we're also using a two-variable random() function includded with compass.
*/

//== Helper functions
@import 'compass';

// Check if value is not a number, eg, NaN or Infinity
@function is-nan($value) {
  @return $value / $value != 1;
}

// Constrain number between two values
@function clip($value, $min : 0.0001, $max : 0.9999) {
  @return if($value > $max, $max, if($value < $min, $min, $value));
}

// Checks if value is within specified bounds, inclusive
@function in-bounds($value, $min : 0, $max : 1) {
  @return if($value >= $min and $value <= $max, true, false);
}

//== Step one: Convert

// Returns an RGB channel processed as XYZ... or partly at least
// See w3.org link for formula
@function xyz($channel) {
  $channel: $channel / 255;
  
  @return if($channel <= 0.03928, $channel / 12.92, pow((($channel + 0.055) / 1.055), 2.4));
}

// Reverse of xyz(). Returns XYZ value to RGB channel
// https://en.wikipedia.org/wiki/SRGB
@function srgb($channel) {  
  @return 255 * if($channel <= 0.0031308, $channel * 12.92, 1.055 * pow($channel, 1/2.4) - 0.055);
}

//== Step two: Measure brightness

// Returns relative luminance of color
// See w3.org link for formula
@function luminance($color) {
  $red:   xyz(red($color));
  $green: xyz(green($color));
  $blue:  xyz(blue($color));
  
  @return $red * 0.2126 + $green * 0.7152 + $blue * 0.0722;
}

//== Step three: Check contrast

// Checks if two colors pass minimum contrast requirements, option to return ratio instead of true/false
// See w3.org link for formula
@function check-contrast($color1, $color2 : #fff, $min-ratio : 'AA', $return-ratio : false) {
  // Accept keywords for ratio
  @if($min-ratio == 'AA' or $min-ratio == 'AAALG') { $min-ratio: 4.5; } 
  @elseif($min-ratio == 'AALG') { $min-ratio: 3; } 
  @elseif($min-ratio == 'AAA') { $min-ratio: 7; }
  
  // Check brightness of each color
  $lum1: luminance($color1);
  $lum2: luminance($color2);
  
  // Measure contrast ratio
  $ratio: (max($lum1, $lum2) + 0.05) / (min($lum1, $lum2) + 0.05);
  
  // Return ratio if option set
  @if($return-ratio) { @return $ratio; }
  
  // Else return boolean
  @return if($ratio >= $min-ratio, true, false);
}

//== Step four: Scale luminance and lightness

// Takes color, scales luminance, spits out new color
@function scale-luminance($color, $target-luminance) {
  // First, scale the channels by the required amount
  $scale: $target-luminance / luminance($color);
  
  // And clip them, so we don't end up dividing by zero... among other things I forget
  $red: clip(xyz(red($color))) * $scale;
  $green: clip(xyz(green($color))) * $scale;
  $blue: clip(xyz(blue($color))) * $scale;
  
  // Sometimes, that's not enough and one channel hits #ff or #00. We'll need to scale the other channels to compensate
  $red-passes: in-bounds($red);
  $green-passes: in-bounds($green);
  $blue-passes: in-bounds($blue);
  
  @if(not $red-passes or not $green-passes or not $blue-passes) {
    // First, pick a channel to be a baseline, so the rest can be expressed as ratios
    $baseline: min($red, $green, $blue);

    // Then set up the variables expressed in terms of the baseline
    $r: $red / $baseline;
    $g: $green / $baseline;
    $b: $blue / $baseline;
    
    // Subtract any channel no longer in bounds
    //-- TODO This needs to DRY. how to dry. help
    @if(not $red-passes) {
      $target-luminance: $target-luminance - 0.2126;
      $r: 0;
    }

    @if(not $green-passes) {
      $target-luminance: $target-luminance - 0.7152;
      $g: 0;
    }

    @if (not $blue-passes) {
      $target-luminance: $target-luminance - 0.0722;
      $b: 0;
    }
    
    // Now get the required difference by using the luminance() formula
    $x: $target-luminance / ($r * 0.2126 + $g * 0.7152 + $b * 0.0722);

    // And multiply the channels by this new per-channel luminance
    @if($red-passes) { $red:   $r * $x; }
    @if($green-passes) { $green: $g * $x; }
    @if($blue-passes) { $blue:  $b * $x; }
  }
  
  // Return the new color
  @return rgb(srgb($red), srgb($green), srgb($blue));
}

// Scales lightness by 0.1% while checking contrast ratio. This is just a last-ditch effort to correct rounding errors
@function scale-light($color1, $color2, $min-ratio, $operation, $iterations) {
  // Loop this function for however many iterations are passed
  @for $n from 1 through $iterations {
    // Return color unchanged if it passes contrast check
    @if(check-contrast($color1, $color2, $min-ratio)) {
      @return $color1;
    } @else {
      // Otherwise use the built-in lighten() and darken() functions, which change the lightness channel (ie, the L in HSL)
      // Our previous scale-luminance() function changes both saturation and lightness
      $color1: if($operation == lighten, lighten($color1, 0.1%), darken($color1, 0.1%));
    }
  }

  // Return the best color we've got
  @return $color1;
}

//== Step six: Fix colors

// Tries to fix contrast by adjusting $color1
@function fix-color($color1, $color2 : #fff, $min-ratio : 'AA', $iterations : 5) {
  // Accept keywords for ratio
  @if($min-ratio == 'AA' or $min-ratio == 'AAALG') { $min-ratio: 4.5; }
  @elseif($min-ratio == 'AALG') { $min-ratio: 3; }
  @elseif($min-ratio == 'AAA') { $min-ratio: 7; }
  
  // If check fails, begin conversion
  @if(not check-contrast($color1, $color2, $min-ratio)) {
    // First get both luminances and clip so #fff and #000 don't break anything
    $lum1: clip(luminance($color1));
    $lum2: clip(luminance($color2));

    // Defaults we'll set later
    $target-luminance: $lum1;
    $operation: '';
    
    // If the same luminance is passed, lighten/darken one to make conversion possible
    @if($lum1 == $lum2) {
      // Darken light colors and lighten dark colors, so we have more room to scale them (eg, we won't hit #fff or #000 before we can fix them)
      @if($lum1 > 0.5) {
        $color1: darken($color1, 1%);
        $lum1: luminance($color1);
      } @else {
        $color1: lighten($color1, 1%);
        $lum1: luminance($color1);
      }
    }
    
    // Now let's get the target luminance. This basically reverses check-contrast(), so we know what luminance to aim for
    @if(max($lum1, $lum2) == $lum1) {
      $target-luminance: (($lum2 + 0.05) * $min-ratio - 0.05);
      $operation: lighten;
    } @else {
      $target-luminance: (($lum2 + 0.05) / $min-ratio - 0.05);
      $operation: darken;
    }
    
    // Skip the whole conversion if we just need #fff or #000
    @if($target-luminance >= 1) { @return #fff; } 
    @elseif ($target-luminance <= 0) { @return #000; } 
    @else {      
      // Scale color by calculated difference to arrive at target luminance
      $color1: scale-luminance($color1, $target-luminance);

      // Try to fix any rounding errors by lightening or darkening
      $color1: scale-light($color1, $color2, $min-ratio, $operation, $iterations);      
    }

  }
  
  // Tada
  @return $color1;
}

// Tries to fix contrast of both colors by weighted balance (0–100)
// 0 = don't change first color, change second color; 
// 100 = change first color, don't change second color
@function fix-contrast($color1, $color2, $min-ratio : 'AA', $balance : 50) {
  @if(not check-contrast($color1, $color2, $min-ratio)) {
    // Fix colors
    $color-fixed-1: fix-color($color1, $color2, $min-ratio);
    $color-fixed-2: fix-color($color2, $color1, $min-ratio);

    // We're just fixing both colors, then mixing back the original color using the native Sass function. Easy-peasy
    $color1: mix($color-fixed-1, $color1, $balance);
    $color2: mix($color2, $color-fixed-2, $balance);

    // If the current configuration doesn't work, try to fix it
    @if (not check-contrast($color1, $color2, $min-ratio)) {
      // This happens if, again, we reach #fff or #000 before we want to
      @if(not in-bounds(luminance($color-fixed-2), 0.00002, 0.99936)) {
        // So we scale the opposite color to compensate
        $color1: fix-color($color1, $color2, $min-ratio);
        @warn "Your settings didn't work. Modifying first color in an attempt to fix."
      }
      @if(not in-bounds(luminance($color-fixed-1), 0.00002, 0.99936)) {
        $color2: fix-color($color2, $color1, $min-ratio);
        @warn "Your settings didn't work. Modifying second color in an attempt to fix."
      }
    }
  }

  // Returns a list with both colors, use nth($result, 1) and nth($result, 2) to get colors. See below for example 
  @return $color1, $color2;
}

// Get the best contrast when given three colors
@function best-contrast($color, $color1, $color2, $ratio1 : 'AA', $ratio2 : $ratio1) {
  @if(not check-contrast($color, $color1, $ratio1) or not check-contrast($color, $color2, $ratio2)) {     
    // First get the luminance of the two static colors
    $lum1: luminance(fix-color($color1, $color1, $ratio1));
    $lum2: luminance(fix-color($color2, $color2, $ratio2));

    // Average the luminance together to get the maximum difference
    $average-lum: ($lum1 + $lum2) / 2;

    // Then set changing color to this luminance
    $color: scale-luminance($color, $average-lum);

    // Warn if it fails contrast check
    @if(not check-contrast($color, $color1, $ratio1)) {
      @warn 'Your color fails to contrast with #{$color1}';
    }

    @if(not check-contrast($color, $color2, $ratio2)) {
      @warn 'Your color fails to contrast with #{$color2}';
    }
  }
  
  @return $color;
}


//====== Helper functions

@function randomColor() {
  $color: hsl(random(0,360), random(0,100), random(0,100));
  @return $color;
}

@mixin show-color($color) {
  background: $color;
  color: if(luminance($color) > 0.55, #000, #fff);
  
  &::after {
    content: '#{$color}';
  }
}

//====== Put in your own settings here
$ratio: random(3,7); // A number between 1 and 21
$balance: random(0, 100); // A number between 0 and 100

// Any valid color
$color1: randomColor(); 
$color2: scale-luminance(randomColor(), luminance($color1) + 0.1);
$color3: randomColor();

.ratio::after { content: '#{$ratio}'; }
.balance::after { content: '#{$balance}'; }

.color-block .color1 { @include show-color($color1); }
.color-block .color2 { @include show-color($color2); }

.fix-color {
  .color:nth-child(2) { @include show-color(fix-color($color1, $color2, $ratio)); }
  .color:nth-child(3) { @include show-color(fix-color($color2, $color1, $ratio)); }
}

.fix-contrast {
  .color:nth-child(2) { @include show-color(nth(fix-contrast($color1, $color2, $ratio, $balance),1)); }
  .color:nth-child(3) { @include show-color(nth(fix-contrast($color1, $color2, $ratio, $balance),2)); }
}

.best-contrast {
  .color:nth-child(2) { @include show-color($color3); }
  .color:nth-child(3) { @include show-color(best-contrast($color3, $color1, $color2, $ratio, $ratio)); }
}

.scale-luminance {
  .color:nth-child(2) { @include show-color(scale-luminance($color1, luminance($color2))); }
}

.check-contrast {
  .result::after { content: '#{check-contrast($color1, $color2, $ratio)}' ;}
}

.luminance {
  .result::after { content: '#{luminance($color1), luminance($color2)}' ;}
}


//====== Page styling, ignore

body {
  padding: 1rem;
  margin: 0 auto;
  max-width: 900px;
  text-align: center;
}

section {
  padding-top: 1rem;
  padding-bottom: 1rem;
}

.color-block { 
  display: inline-block; 
  padding: 0 1rem;
  vertical-align: top;
}

.color,
.result {
  display: inline-flex;
  flex-wrap: wrap;
  align-content: center;
  align-items: center;
  justify-content: center;
  
  background: #eee;
  box-sizing: border-box;
  border: 1px solid #ddd;
  
  margin: 0.5rem;
  position: relative;
  height: 100px;
  width: 100px;
  
  &::after {
    display: block;
    font-weight: bolder;
    width: 100%;
    line-height: 1;
  }
}

.color {
  box-shadow: inset 0 0 0 8px #fff;
}

.result {
  padding: 0.5rem;
}

br { clear: both; }
              
            
!

JS

              
                
              
            
!
999px

Console