Scaling a color based on a target contrast ratio












0















I am trying to create a Sass function that receives a foreground color and background color and calculates the contrast ratio. From there (and the part I'm stuck on) is that it would simply return the foreground color if it meets the target contrast ratio, but if it doesn't it would lighten or darken the foreground color to meet the target contrast ratio.



For example, if the background supplied was #000 and the foreground supplied was #444 (a contrast ratio of 2.15), this function would lighten the foreground to #757575 and return that color.



I've got everything working except for the part where I need to reverse the contrast calculation. My initial thought was to approach it with what percentage it was away from target and simply lighten/darken (depending on which color was originally darker) by 100 minus the percent difference. This approach, in hindsight, was a little naive and I'm afraid some more advanced math will be involved.



Here is what I created so far (and here is a simplified fiddle):



@function wcag-color($bg, $fg, $size: 16px, $level: "aa"){
@if ( $level == "aa" ){
$wcag_contrast_ratio: 4.5; //For text smaller than 18px
@if ( $size >= 19 ){
$wcag_contrast_ratio: 3; //For text larger than 19px
}
}

@if ( $level == "aaa" ){
$wcag_contrast_ratio: 7; //For text smaller than 18px
@if ( $size >= 19 ){
$wcag_contrast_ratio: 4.5; //For text larger than 19px
}
}

$actual_contrast_ratio: contrast($bg, $fg); //This function returns the contrast between the two colors.

@if ( $actual_contrast_ratio > $wcag_contrast_ratio ){
@return $fg; //Foreground color is acceptable
}

//Scale the lightness of the foreground to meet requested WCAG contrast ratio
$difference: 100 - $actual_contrast_ratio / $wcag_contrast_ratio * 100; //There is more to it than this...

//Edit: here are a few new lines to ponder. This assumes BG is darker than FG (would need to add a condition to compare luminance of each).
$acceptable_luminance: luminance($bg)*$wcag_contrast_ratio; //What the luminance of the FG must be to comply
$difference: ($acceptable_luminance - luminance($fg)); //How far away the FG luminance actually is (not sure if this helps anything...)

@return scale-color($fg, $lightness: $difference); //Unfortunately luminance is not the same as lightness.
}


Notice the commented line "There is more to it than this..." – that is where I need to reverse my contrast formula, but I'd love if there was a simpler formula to use since I already know what the target contrast ratio is.



I've been thinking about this for a few days an I'm stumped. I'd prefer to avoid a guess-and-check method by looping through 1% lightened/darkened colors and testing each individually for their contrast ratio– that would work, but I'm sure there is a more optimal solution.



This was my reference for my initial functions (contrast and luminance) and was very helpful: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796



Note: I am not using Compass or any other Sass libraries.



Edit: Here is a simplified fiddle for reference: https://www.sassmeister.com/gist/445836123feb42885a0cf7f4709261ff










share|improve this question





























    0















    I am trying to create a Sass function that receives a foreground color and background color and calculates the contrast ratio. From there (and the part I'm stuck on) is that it would simply return the foreground color if it meets the target contrast ratio, but if it doesn't it would lighten or darken the foreground color to meet the target contrast ratio.



    For example, if the background supplied was #000 and the foreground supplied was #444 (a contrast ratio of 2.15), this function would lighten the foreground to #757575 and return that color.



    I've got everything working except for the part where I need to reverse the contrast calculation. My initial thought was to approach it with what percentage it was away from target and simply lighten/darken (depending on which color was originally darker) by 100 minus the percent difference. This approach, in hindsight, was a little naive and I'm afraid some more advanced math will be involved.



    Here is what I created so far (and here is a simplified fiddle):



    @function wcag-color($bg, $fg, $size: 16px, $level: "aa"){
    @if ( $level == "aa" ){
    $wcag_contrast_ratio: 4.5; //For text smaller than 18px
    @if ( $size >= 19 ){
    $wcag_contrast_ratio: 3; //For text larger than 19px
    }
    }

    @if ( $level == "aaa" ){
    $wcag_contrast_ratio: 7; //For text smaller than 18px
    @if ( $size >= 19 ){
    $wcag_contrast_ratio: 4.5; //For text larger than 19px
    }
    }

    $actual_contrast_ratio: contrast($bg, $fg); //This function returns the contrast between the two colors.

    @if ( $actual_contrast_ratio > $wcag_contrast_ratio ){
    @return $fg; //Foreground color is acceptable
    }

    //Scale the lightness of the foreground to meet requested WCAG contrast ratio
    $difference: 100 - $actual_contrast_ratio / $wcag_contrast_ratio * 100; //There is more to it than this...

    //Edit: here are a few new lines to ponder. This assumes BG is darker than FG (would need to add a condition to compare luminance of each).
    $acceptable_luminance: luminance($bg)*$wcag_contrast_ratio; //What the luminance of the FG must be to comply
    $difference: ($acceptable_luminance - luminance($fg)); //How far away the FG luminance actually is (not sure if this helps anything...)

    @return scale-color($fg, $lightness: $difference); //Unfortunately luminance is not the same as lightness.
    }


    Notice the commented line "There is more to it than this..." – that is where I need to reverse my contrast formula, but I'd love if there was a simpler formula to use since I already know what the target contrast ratio is.



    I've been thinking about this for a few days an I'm stumped. I'd prefer to avoid a guess-and-check method by looping through 1% lightened/darkened colors and testing each individually for their contrast ratio– that would work, but I'm sure there is a more optimal solution.



    This was my reference for my initial functions (contrast and luminance) and was very helpful: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796



    Note: I am not using Compass or any other Sass libraries.



    Edit: Here is a simplified fiddle for reference: https://www.sassmeister.com/gist/445836123feb42885a0cf7f4709261ff










    share|improve this question



























      0












      0








      0








      I am trying to create a Sass function that receives a foreground color and background color and calculates the contrast ratio. From there (and the part I'm stuck on) is that it would simply return the foreground color if it meets the target contrast ratio, but if it doesn't it would lighten or darken the foreground color to meet the target contrast ratio.



      For example, if the background supplied was #000 and the foreground supplied was #444 (a contrast ratio of 2.15), this function would lighten the foreground to #757575 and return that color.



      I've got everything working except for the part where I need to reverse the contrast calculation. My initial thought was to approach it with what percentage it was away from target and simply lighten/darken (depending on which color was originally darker) by 100 minus the percent difference. This approach, in hindsight, was a little naive and I'm afraid some more advanced math will be involved.



      Here is what I created so far (and here is a simplified fiddle):



      @function wcag-color($bg, $fg, $size: 16px, $level: "aa"){
      @if ( $level == "aa" ){
      $wcag_contrast_ratio: 4.5; //For text smaller than 18px
      @if ( $size >= 19 ){
      $wcag_contrast_ratio: 3; //For text larger than 19px
      }
      }

      @if ( $level == "aaa" ){
      $wcag_contrast_ratio: 7; //For text smaller than 18px
      @if ( $size >= 19 ){
      $wcag_contrast_ratio: 4.5; //For text larger than 19px
      }
      }

      $actual_contrast_ratio: contrast($bg, $fg); //This function returns the contrast between the two colors.

      @if ( $actual_contrast_ratio > $wcag_contrast_ratio ){
      @return $fg; //Foreground color is acceptable
      }

      //Scale the lightness of the foreground to meet requested WCAG contrast ratio
      $difference: 100 - $actual_contrast_ratio / $wcag_contrast_ratio * 100; //There is more to it than this...

      //Edit: here are a few new lines to ponder. This assumes BG is darker than FG (would need to add a condition to compare luminance of each).
      $acceptable_luminance: luminance($bg)*$wcag_contrast_ratio; //What the luminance of the FG must be to comply
      $difference: ($acceptable_luminance - luminance($fg)); //How far away the FG luminance actually is (not sure if this helps anything...)

      @return scale-color($fg, $lightness: $difference); //Unfortunately luminance is not the same as lightness.
      }


      Notice the commented line "There is more to it than this..." – that is where I need to reverse my contrast formula, but I'd love if there was a simpler formula to use since I already know what the target contrast ratio is.



      I've been thinking about this for a few days an I'm stumped. I'd prefer to avoid a guess-and-check method by looping through 1% lightened/darkened colors and testing each individually for their contrast ratio– that would work, but I'm sure there is a more optimal solution.



      This was my reference for my initial functions (contrast and luminance) and was very helpful: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796



      Note: I am not using Compass or any other Sass libraries.



      Edit: Here is a simplified fiddle for reference: https://www.sassmeister.com/gist/445836123feb42885a0cf7f4709261ff










      share|improve this question
















      I am trying to create a Sass function that receives a foreground color and background color and calculates the contrast ratio. From there (and the part I'm stuck on) is that it would simply return the foreground color if it meets the target contrast ratio, but if it doesn't it would lighten or darken the foreground color to meet the target contrast ratio.



      For example, if the background supplied was #000 and the foreground supplied was #444 (a contrast ratio of 2.15), this function would lighten the foreground to #757575 and return that color.



      I've got everything working except for the part where I need to reverse the contrast calculation. My initial thought was to approach it with what percentage it was away from target and simply lighten/darken (depending on which color was originally darker) by 100 minus the percent difference. This approach, in hindsight, was a little naive and I'm afraid some more advanced math will be involved.



      Here is what I created so far (and here is a simplified fiddle):



      @function wcag-color($bg, $fg, $size: 16px, $level: "aa"){
      @if ( $level == "aa" ){
      $wcag_contrast_ratio: 4.5; //For text smaller than 18px
      @if ( $size >= 19 ){
      $wcag_contrast_ratio: 3; //For text larger than 19px
      }
      }

      @if ( $level == "aaa" ){
      $wcag_contrast_ratio: 7; //For text smaller than 18px
      @if ( $size >= 19 ){
      $wcag_contrast_ratio: 4.5; //For text larger than 19px
      }
      }

      $actual_contrast_ratio: contrast($bg, $fg); //This function returns the contrast between the two colors.

      @if ( $actual_contrast_ratio > $wcag_contrast_ratio ){
      @return $fg; //Foreground color is acceptable
      }

      //Scale the lightness of the foreground to meet requested WCAG contrast ratio
      $difference: 100 - $actual_contrast_ratio / $wcag_contrast_ratio * 100; //There is more to it than this...

      //Edit: here are a few new lines to ponder. This assumes BG is darker than FG (would need to add a condition to compare luminance of each).
      $acceptable_luminance: luminance($bg)*$wcag_contrast_ratio; //What the luminance of the FG must be to comply
      $difference: ($acceptable_luminance - luminance($fg)); //How far away the FG luminance actually is (not sure if this helps anything...)

      @return scale-color($fg, $lightness: $difference); //Unfortunately luminance is not the same as lightness.
      }


      Notice the commented line "There is more to it than this..." – that is where I need to reverse my contrast formula, but I'd love if there was a simpler formula to use since I already know what the target contrast ratio is.



      I've been thinking about this for a few days an I'm stumped. I'd prefer to avoid a guess-and-check method by looping through 1% lightened/darkened colors and testing each individually for their contrast ratio– that would work, but I'm sure there is a more optimal solution.



      This was my reference for my initial functions (contrast and luminance) and was very helpful: https://medium.com/dev-channel/using-sass-to-automatically-pick-text-colors-4ba7645d2796



      Note: I am not using Compass or any other Sass libraries.



      Edit: Here is a simplified fiddle for reference: https://www.sassmeister.com/gist/445836123feb42885a0cf7f4709261ff







      css colors sass wcag






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Jan 18 at 14:19







      GreatBlakes

















      asked Jan 17 at 18:11









      GreatBlakesGreatBlakes

      2,78931525




      2,78931525
























          1 Answer
          1






          active

          oldest

          votes


















          2














          So given #000000 and #444444, you can calculate the contrast ratio (2.15 in this case). The math is pretty straightforward, albeit a little hairy. (See the "relative luminance" definition.)



          Now you want to go backwards? If you have #000000 and want a ratio of 4.5, starting with #444444, what should the color be? Is that what




          I need to reverse my contrast formula




          means?



          It's a little complicated because you're solving for 3 variables, the red, green, and blue components, plus the luminance formula doesn't treat the red, green and blue equally. It's using 21.25% red, 71.5% green, and 7.25% blue.



          Plus, the luminance formula isn't a simple linear formula so you can't just take a percentage short of luminance and bump the color value by that same percentage.



          For example, in your case, the ratio was 2.15 but you need it to be 4.5. 2.15 is 108% short of 4.5, the desired value.



          However, if you look at your original RGB values #444444 and you calculated it needed to be #757575 (in order to have a 4.5 ratio), then if you treat those RGB values as simple numbers (and convert to decimal), then #444444 (4473924) is 72% short of #757575 (7697781).



          So you have a disconnect that your ratio is short by 108% but your RGB values are short by 72%. Thus you can't do a simple linear equation.



          (The numbers aren't quite exact since #757575 gives you a 4.56 ratio, not an exact 4.5 ratio. If you use #747474, you get a 4.49 ratio, which is just a smidge too small for WCAG compliance but is closer to 4.5 than 4.56. However, #444444 is 71% short of #747474, so it's still not the same as 2.15 being 108% short of 4.5, so the basic concept still applies.)



          Just for fun, I looked at the values of 0x11111 through 0x666666, incrementing by 0x111111, and calculated the contrast ratio. There weren't enough points on the graph so I added a color halfway between 0x111111 and 0x222222, then halfway between 0x222222 and 0x333333, etc.



          RGB     contrast  % from 4.5  % from 0x747474
          111111 1.11 305.41% 582.35%
          191919 1.19 278.15% 364.00%
          222222 1.32 240.91% 241.18%
          2a2a2a 1.46 208.22% 176.19%
          333333 1.66 171.08% 127.45%
          3b3b3b 1.87 140.64% 96.61%
          444444 2.16 108.33% 70.59%
          4c4c4c 2.45 83.67% 52.63%
          555555 2.82 59.57% 36.47%
          5d5d5d 3.19 41.07% 24.73%
          666666 3.66 22.95% 13.73%
          6e6e6e 4.12 9.22% 5.45%



          graph of contrast ratios



          As you can see, the lines interset at the 3rd data point then converge toward each other. I'm sure there's a formula in there so you could take the contrast percentage, do some (probably logarithmic) function on it and get the percentage needed to change the color.



          This would be a fascinating math problem that I currently don't have time to play with.



          Update Jan 18, 2019



          I got it to work going backwards, but it doesn't handle edge cases such as when making a dark color darker but you've already reached the max (or a light color lighter but you reached the max). But maybe you can play with it.



          Test case





          • #ee0add for the light color (magenta-ish)


          • #445566 for the dark color (dark gray)

          • contrast ratio 2.09


          When computing the "relative luminance" of a color, it has a conditional statement.



          if X <= 0.03928 then 
          X = X/12.92
          else
          X = ((X+0.055)/1.055) ^ 2.4


          Before X is used in that condition, it's divided by 255 to normalize the value between 0 and 1. So if you take the conditional value, 0.03928, and multiply by 255, you get 10.0164. Since RGB values must be integers, that means an RGB component of 10 (0x0A) or less will go through the "if" and anything 11 (0x0B) or bigger will go through the "else". So in my test case values, I wanted one of the color parts to be 10 (0x0A) (#EE0ADD).



          The relative luminance for #ee0add is 0.23614683378171950172526363525113 (0.236)



          The relative luminance for #445566 is 0.0868525191131797135799815832377 (0.0868)



          The "contrast ratio" is
          (0.236 + .05) / (0.0868 + .05) = 2.09



          (You can verify this ratio on https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=445566)



          If we want a ratio of 4.5, and we want #ee0add to not change, then we have to adjust #445566. That means you need to solve for:



          4.5 = (0.236 + .05) / (XX + .05)


          So the second luminance value (XX) needs to be 0.01358818528482655593894747450025 (0.0136)



          The original second luminance value was 0.0868525191131797135799815832377 (0.0868), so to get 0.01358818528482655593894747450025 (0.0136), we need to multiply the original by 0.15645125119651910313960717062698 (0.0136 / 0.0868 = 0.156) (or 15.6% of the original value)



          If we apply that 15.6% to each of the R, G, and B relative luminance values, and then work through the conditional statement above backwards, you can get the RGB values.



          Original luminance for #445566



          r = 0x44 = 68 
          g = 0x55 = 85
          b = 0x66 = 102

          r1 = 68 / 255 = 0.26666666666666666666666666666667
          g1 = 85 / 255 = 0.33333333333333333333333333333333
          b1 = 102 / 255 = 0.4

          r2 = ((.267 + .055) / 1.055)^^2.4 = 0.05780543019106721120703816752337
          g2 = ((.333 + .055) / 1.055)^^2.4 = 0.09084171118340767766490119106965
          b2 = ((.400 + .055) / 1.055)^^2.4 = 0.13286832155381791428570549818868

          l = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2
          = 0.0868525191131797135799815832377


          Working backwards, take 15.6% of the r2, g2, and b2 values



          r2 = 0.05780543019106721120703816752337 * 15.6% = 0.00904373187934550551617004082875
          g2 = 0.09084171118340767766490119106965 * 15.6% = 0.01421229937547695322310904970549
          b2 = 0.13286832155381791428570549818868 * 15.6% = 0.02078741515147623990363062978804


          Now undo that mess with ^^2.4 and the other stuff




          • to undo X^^2.4 you have to do the inverse, X^^(1/2.4) or X^^(0.4167)

          • then multiply by 1.055

          • then subtract 0.055

          • then multiply by 255


          pow( 0.00904373187934550551617004082875, 1/2.4) = 0.14075965680504652191078668676178
          pow( 0.01421229937547695322310904970549, 1/2.4) = 0.16993264267137740728089791717873
          pow( 0.02078741515147623990363062978804, 1/2.4) = 0.19910562853770829265100914759565


          multiply by 1.055



          0.14075965680504652191078668676178 * 1.055 = 0.14850143792932408061587995453368
          0.16993264267137740728089791717873 * 1.055 = 0.17927893801830316468134730262356
          0.19910562853770829265100914759565 * 1.055 = 0.21005643810728224874681465071341


          subtract 0.055



          0.14850143792932408061587995453368 - 0.055 = 0.09350143792932408061587995453368
          0.17927893801830316468134730262356 - 0.055 = 0.12427893801830316468134730262356
          0.21005643810728224874681465071341 - 0.055 = 0.15505643810728224874681465071341


          multiply by 255



          0.09350143792932408061587995453368 * 255 = 23.842866671977640557049388406088 = 24 = 0x18
          0.12427893801830316468134730262356 * 255 = 31.691129194667306993743562169008 = 32 = 0x20
          0.15505643810728224874681465071341 * 255 = 39.53939171735697343043773593192 = 40 = 0x28


          So the darker color is #182028. There are probably some rounding errors but if you check the original foreground color, #ee0add with the new color, #182028, you get a contrast ratio of 4.48. Just shy of 4.5, but like I said, probably some rounding errors.



          https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=182028



          I tried doing the same thing with #ee0add, keeping #445566 the same, but when going backwards and getting to the last step where you multiply by 255, I got numbers greater than 255, which are not valid RGB components (they can only go up to 0xFF). If I stopped the number at 255 then took the difference and added it to the smallest color value, I got a decent color but the ratio was 5.04, overshooting 4.5. I can post that math too if you want.






          share|improve this answer


























          • Thank you very much for your info/visuals. Yeah, I ran into some of the same things you were describing last night when trying to rework the formula. I knew that I could isolate L1 by multiplying L2 by the target contrast ratio and then I ran into the problem that you mentioned where I would then need to solve for red, green, and blue (in a non-equal way). However, since I do know the RGB of the hue and saturation (and original lightness), I thought that may help. Thank you for that graph– I actually was writing down values on a piece of paper, but that graph helps so much more.

            – GreatBlakes
            Jan 18 at 14:17











          • take a look at the color contrast checker on webaim.org/resources/contrastchecker. they have a lighten/darken slider that adjusts the color. it won't automatically bump it to a color that has sufficient contrast, but you can look at their code to see how they adjust the color lighter or darker. look at webaim.org/resources/contrastchecker/contrast.js. it's converting the rgb to hsl, which sounds like what you're doing. there must be a way to go backwards to generate a sufficient color.

            – slugolicious
            Jan 18 at 20:36











          • Wow- thanks so much for that update! I really appreciate your math help on this!

            – GreatBlakes
            Jan 21 at 15:34











          Your Answer






          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "1"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          autoActivateHeartbeat: false,
          convertImagesToLinks: true,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: 10,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54241873%2fscaling-a-color-based-on-a-target-contrast-ratio%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          1 Answer
          1






          active

          oldest

          votes








          1 Answer
          1






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes









          2














          So given #000000 and #444444, you can calculate the contrast ratio (2.15 in this case). The math is pretty straightforward, albeit a little hairy. (See the "relative luminance" definition.)



          Now you want to go backwards? If you have #000000 and want a ratio of 4.5, starting with #444444, what should the color be? Is that what




          I need to reverse my contrast formula




          means?



          It's a little complicated because you're solving for 3 variables, the red, green, and blue components, plus the luminance formula doesn't treat the red, green and blue equally. It's using 21.25% red, 71.5% green, and 7.25% blue.



          Plus, the luminance formula isn't a simple linear formula so you can't just take a percentage short of luminance and bump the color value by that same percentage.



          For example, in your case, the ratio was 2.15 but you need it to be 4.5. 2.15 is 108% short of 4.5, the desired value.



          However, if you look at your original RGB values #444444 and you calculated it needed to be #757575 (in order to have a 4.5 ratio), then if you treat those RGB values as simple numbers (and convert to decimal), then #444444 (4473924) is 72% short of #757575 (7697781).



          So you have a disconnect that your ratio is short by 108% but your RGB values are short by 72%. Thus you can't do a simple linear equation.



          (The numbers aren't quite exact since #757575 gives you a 4.56 ratio, not an exact 4.5 ratio. If you use #747474, you get a 4.49 ratio, which is just a smidge too small for WCAG compliance but is closer to 4.5 than 4.56. However, #444444 is 71% short of #747474, so it's still not the same as 2.15 being 108% short of 4.5, so the basic concept still applies.)



          Just for fun, I looked at the values of 0x11111 through 0x666666, incrementing by 0x111111, and calculated the contrast ratio. There weren't enough points on the graph so I added a color halfway between 0x111111 and 0x222222, then halfway between 0x222222 and 0x333333, etc.



          RGB     contrast  % from 4.5  % from 0x747474
          111111 1.11 305.41% 582.35%
          191919 1.19 278.15% 364.00%
          222222 1.32 240.91% 241.18%
          2a2a2a 1.46 208.22% 176.19%
          333333 1.66 171.08% 127.45%
          3b3b3b 1.87 140.64% 96.61%
          444444 2.16 108.33% 70.59%
          4c4c4c 2.45 83.67% 52.63%
          555555 2.82 59.57% 36.47%
          5d5d5d 3.19 41.07% 24.73%
          666666 3.66 22.95% 13.73%
          6e6e6e 4.12 9.22% 5.45%



          graph of contrast ratios



          As you can see, the lines interset at the 3rd data point then converge toward each other. I'm sure there's a formula in there so you could take the contrast percentage, do some (probably logarithmic) function on it and get the percentage needed to change the color.



          This would be a fascinating math problem that I currently don't have time to play with.



          Update Jan 18, 2019



          I got it to work going backwards, but it doesn't handle edge cases such as when making a dark color darker but you've already reached the max (or a light color lighter but you reached the max). But maybe you can play with it.



          Test case





          • #ee0add for the light color (magenta-ish)


          • #445566 for the dark color (dark gray)

          • contrast ratio 2.09


          When computing the "relative luminance" of a color, it has a conditional statement.



          if X <= 0.03928 then 
          X = X/12.92
          else
          X = ((X+0.055)/1.055) ^ 2.4


          Before X is used in that condition, it's divided by 255 to normalize the value between 0 and 1. So if you take the conditional value, 0.03928, and multiply by 255, you get 10.0164. Since RGB values must be integers, that means an RGB component of 10 (0x0A) or less will go through the "if" and anything 11 (0x0B) or bigger will go through the "else". So in my test case values, I wanted one of the color parts to be 10 (0x0A) (#EE0ADD).



          The relative luminance for #ee0add is 0.23614683378171950172526363525113 (0.236)



          The relative luminance for #445566 is 0.0868525191131797135799815832377 (0.0868)



          The "contrast ratio" is
          (0.236 + .05) / (0.0868 + .05) = 2.09



          (You can verify this ratio on https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=445566)



          If we want a ratio of 4.5, and we want #ee0add to not change, then we have to adjust #445566. That means you need to solve for:



          4.5 = (0.236 + .05) / (XX + .05)


          So the second luminance value (XX) needs to be 0.01358818528482655593894747450025 (0.0136)



          The original second luminance value was 0.0868525191131797135799815832377 (0.0868), so to get 0.01358818528482655593894747450025 (0.0136), we need to multiply the original by 0.15645125119651910313960717062698 (0.0136 / 0.0868 = 0.156) (or 15.6% of the original value)



          If we apply that 15.6% to each of the R, G, and B relative luminance values, and then work through the conditional statement above backwards, you can get the RGB values.



          Original luminance for #445566



          r = 0x44 = 68 
          g = 0x55 = 85
          b = 0x66 = 102

          r1 = 68 / 255 = 0.26666666666666666666666666666667
          g1 = 85 / 255 = 0.33333333333333333333333333333333
          b1 = 102 / 255 = 0.4

          r2 = ((.267 + .055) / 1.055)^^2.4 = 0.05780543019106721120703816752337
          g2 = ((.333 + .055) / 1.055)^^2.4 = 0.09084171118340767766490119106965
          b2 = ((.400 + .055) / 1.055)^^2.4 = 0.13286832155381791428570549818868

          l = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2
          = 0.0868525191131797135799815832377


          Working backwards, take 15.6% of the r2, g2, and b2 values



          r2 = 0.05780543019106721120703816752337 * 15.6% = 0.00904373187934550551617004082875
          g2 = 0.09084171118340767766490119106965 * 15.6% = 0.01421229937547695322310904970549
          b2 = 0.13286832155381791428570549818868 * 15.6% = 0.02078741515147623990363062978804


          Now undo that mess with ^^2.4 and the other stuff




          • to undo X^^2.4 you have to do the inverse, X^^(1/2.4) or X^^(0.4167)

          • then multiply by 1.055

          • then subtract 0.055

          • then multiply by 255


          pow( 0.00904373187934550551617004082875, 1/2.4) = 0.14075965680504652191078668676178
          pow( 0.01421229937547695322310904970549, 1/2.4) = 0.16993264267137740728089791717873
          pow( 0.02078741515147623990363062978804, 1/2.4) = 0.19910562853770829265100914759565


          multiply by 1.055



          0.14075965680504652191078668676178 * 1.055 = 0.14850143792932408061587995453368
          0.16993264267137740728089791717873 * 1.055 = 0.17927893801830316468134730262356
          0.19910562853770829265100914759565 * 1.055 = 0.21005643810728224874681465071341


          subtract 0.055



          0.14850143792932408061587995453368 - 0.055 = 0.09350143792932408061587995453368
          0.17927893801830316468134730262356 - 0.055 = 0.12427893801830316468134730262356
          0.21005643810728224874681465071341 - 0.055 = 0.15505643810728224874681465071341


          multiply by 255



          0.09350143792932408061587995453368 * 255 = 23.842866671977640557049388406088 = 24 = 0x18
          0.12427893801830316468134730262356 * 255 = 31.691129194667306993743562169008 = 32 = 0x20
          0.15505643810728224874681465071341 * 255 = 39.53939171735697343043773593192 = 40 = 0x28


          So the darker color is #182028. There are probably some rounding errors but if you check the original foreground color, #ee0add with the new color, #182028, you get a contrast ratio of 4.48. Just shy of 4.5, but like I said, probably some rounding errors.



          https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=182028



          I tried doing the same thing with #ee0add, keeping #445566 the same, but when going backwards and getting to the last step where you multiply by 255, I got numbers greater than 255, which are not valid RGB components (they can only go up to 0xFF). If I stopped the number at 255 then took the difference and added it to the smallest color value, I got a decent color but the ratio was 5.04, overshooting 4.5. I can post that math too if you want.






          share|improve this answer


























          • Thank you very much for your info/visuals. Yeah, I ran into some of the same things you were describing last night when trying to rework the formula. I knew that I could isolate L1 by multiplying L2 by the target contrast ratio and then I ran into the problem that you mentioned where I would then need to solve for red, green, and blue (in a non-equal way). However, since I do know the RGB of the hue and saturation (and original lightness), I thought that may help. Thank you for that graph– I actually was writing down values on a piece of paper, but that graph helps so much more.

            – GreatBlakes
            Jan 18 at 14:17











          • take a look at the color contrast checker on webaim.org/resources/contrastchecker. they have a lighten/darken slider that adjusts the color. it won't automatically bump it to a color that has sufficient contrast, but you can look at their code to see how they adjust the color lighter or darker. look at webaim.org/resources/contrastchecker/contrast.js. it's converting the rgb to hsl, which sounds like what you're doing. there must be a way to go backwards to generate a sufficient color.

            – slugolicious
            Jan 18 at 20:36











          • Wow- thanks so much for that update! I really appreciate your math help on this!

            – GreatBlakes
            Jan 21 at 15:34
















          2














          So given #000000 and #444444, you can calculate the contrast ratio (2.15 in this case). The math is pretty straightforward, albeit a little hairy. (See the "relative luminance" definition.)



          Now you want to go backwards? If you have #000000 and want a ratio of 4.5, starting with #444444, what should the color be? Is that what




          I need to reverse my contrast formula




          means?



          It's a little complicated because you're solving for 3 variables, the red, green, and blue components, plus the luminance formula doesn't treat the red, green and blue equally. It's using 21.25% red, 71.5% green, and 7.25% blue.



          Plus, the luminance formula isn't a simple linear formula so you can't just take a percentage short of luminance and bump the color value by that same percentage.



          For example, in your case, the ratio was 2.15 but you need it to be 4.5. 2.15 is 108% short of 4.5, the desired value.



          However, if you look at your original RGB values #444444 and you calculated it needed to be #757575 (in order to have a 4.5 ratio), then if you treat those RGB values as simple numbers (and convert to decimal), then #444444 (4473924) is 72% short of #757575 (7697781).



          So you have a disconnect that your ratio is short by 108% but your RGB values are short by 72%. Thus you can't do a simple linear equation.



          (The numbers aren't quite exact since #757575 gives you a 4.56 ratio, not an exact 4.5 ratio. If you use #747474, you get a 4.49 ratio, which is just a smidge too small for WCAG compliance but is closer to 4.5 than 4.56. However, #444444 is 71% short of #747474, so it's still not the same as 2.15 being 108% short of 4.5, so the basic concept still applies.)



          Just for fun, I looked at the values of 0x11111 through 0x666666, incrementing by 0x111111, and calculated the contrast ratio. There weren't enough points on the graph so I added a color halfway between 0x111111 and 0x222222, then halfway between 0x222222 and 0x333333, etc.



          RGB     contrast  % from 4.5  % from 0x747474
          111111 1.11 305.41% 582.35%
          191919 1.19 278.15% 364.00%
          222222 1.32 240.91% 241.18%
          2a2a2a 1.46 208.22% 176.19%
          333333 1.66 171.08% 127.45%
          3b3b3b 1.87 140.64% 96.61%
          444444 2.16 108.33% 70.59%
          4c4c4c 2.45 83.67% 52.63%
          555555 2.82 59.57% 36.47%
          5d5d5d 3.19 41.07% 24.73%
          666666 3.66 22.95% 13.73%
          6e6e6e 4.12 9.22% 5.45%



          graph of contrast ratios



          As you can see, the lines interset at the 3rd data point then converge toward each other. I'm sure there's a formula in there so you could take the contrast percentage, do some (probably logarithmic) function on it and get the percentage needed to change the color.



          This would be a fascinating math problem that I currently don't have time to play with.



          Update Jan 18, 2019



          I got it to work going backwards, but it doesn't handle edge cases such as when making a dark color darker but you've already reached the max (or a light color lighter but you reached the max). But maybe you can play with it.



          Test case





          • #ee0add for the light color (magenta-ish)


          • #445566 for the dark color (dark gray)

          • contrast ratio 2.09


          When computing the "relative luminance" of a color, it has a conditional statement.



          if X <= 0.03928 then 
          X = X/12.92
          else
          X = ((X+0.055)/1.055) ^ 2.4


          Before X is used in that condition, it's divided by 255 to normalize the value between 0 and 1. So if you take the conditional value, 0.03928, and multiply by 255, you get 10.0164. Since RGB values must be integers, that means an RGB component of 10 (0x0A) or less will go through the "if" and anything 11 (0x0B) or bigger will go through the "else". So in my test case values, I wanted one of the color parts to be 10 (0x0A) (#EE0ADD).



          The relative luminance for #ee0add is 0.23614683378171950172526363525113 (0.236)



          The relative luminance for #445566 is 0.0868525191131797135799815832377 (0.0868)



          The "contrast ratio" is
          (0.236 + .05) / (0.0868 + .05) = 2.09



          (You can verify this ratio on https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=445566)



          If we want a ratio of 4.5, and we want #ee0add to not change, then we have to adjust #445566. That means you need to solve for:



          4.5 = (0.236 + .05) / (XX + .05)


          So the second luminance value (XX) needs to be 0.01358818528482655593894747450025 (0.0136)



          The original second luminance value was 0.0868525191131797135799815832377 (0.0868), so to get 0.01358818528482655593894747450025 (0.0136), we need to multiply the original by 0.15645125119651910313960717062698 (0.0136 / 0.0868 = 0.156) (or 15.6% of the original value)



          If we apply that 15.6% to each of the R, G, and B relative luminance values, and then work through the conditional statement above backwards, you can get the RGB values.



          Original luminance for #445566



          r = 0x44 = 68 
          g = 0x55 = 85
          b = 0x66 = 102

          r1 = 68 / 255 = 0.26666666666666666666666666666667
          g1 = 85 / 255 = 0.33333333333333333333333333333333
          b1 = 102 / 255 = 0.4

          r2 = ((.267 + .055) / 1.055)^^2.4 = 0.05780543019106721120703816752337
          g2 = ((.333 + .055) / 1.055)^^2.4 = 0.09084171118340767766490119106965
          b2 = ((.400 + .055) / 1.055)^^2.4 = 0.13286832155381791428570549818868

          l = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2
          = 0.0868525191131797135799815832377


          Working backwards, take 15.6% of the r2, g2, and b2 values



          r2 = 0.05780543019106721120703816752337 * 15.6% = 0.00904373187934550551617004082875
          g2 = 0.09084171118340767766490119106965 * 15.6% = 0.01421229937547695322310904970549
          b2 = 0.13286832155381791428570549818868 * 15.6% = 0.02078741515147623990363062978804


          Now undo that mess with ^^2.4 and the other stuff




          • to undo X^^2.4 you have to do the inverse, X^^(1/2.4) or X^^(0.4167)

          • then multiply by 1.055

          • then subtract 0.055

          • then multiply by 255


          pow( 0.00904373187934550551617004082875, 1/2.4) = 0.14075965680504652191078668676178
          pow( 0.01421229937547695322310904970549, 1/2.4) = 0.16993264267137740728089791717873
          pow( 0.02078741515147623990363062978804, 1/2.4) = 0.19910562853770829265100914759565


          multiply by 1.055



          0.14075965680504652191078668676178 * 1.055 = 0.14850143792932408061587995453368
          0.16993264267137740728089791717873 * 1.055 = 0.17927893801830316468134730262356
          0.19910562853770829265100914759565 * 1.055 = 0.21005643810728224874681465071341


          subtract 0.055



          0.14850143792932408061587995453368 - 0.055 = 0.09350143792932408061587995453368
          0.17927893801830316468134730262356 - 0.055 = 0.12427893801830316468134730262356
          0.21005643810728224874681465071341 - 0.055 = 0.15505643810728224874681465071341


          multiply by 255



          0.09350143792932408061587995453368 * 255 = 23.842866671977640557049388406088 = 24 = 0x18
          0.12427893801830316468134730262356 * 255 = 31.691129194667306993743562169008 = 32 = 0x20
          0.15505643810728224874681465071341 * 255 = 39.53939171735697343043773593192 = 40 = 0x28


          So the darker color is #182028. There are probably some rounding errors but if you check the original foreground color, #ee0add with the new color, #182028, you get a contrast ratio of 4.48. Just shy of 4.5, but like I said, probably some rounding errors.



          https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=182028



          I tried doing the same thing with #ee0add, keeping #445566 the same, but when going backwards and getting to the last step where you multiply by 255, I got numbers greater than 255, which are not valid RGB components (they can only go up to 0xFF). If I stopped the number at 255 then took the difference and added it to the smallest color value, I got a decent color but the ratio was 5.04, overshooting 4.5. I can post that math too if you want.






          share|improve this answer


























          • Thank you very much for your info/visuals. Yeah, I ran into some of the same things you were describing last night when trying to rework the formula. I knew that I could isolate L1 by multiplying L2 by the target contrast ratio and then I ran into the problem that you mentioned where I would then need to solve for red, green, and blue (in a non-equal way). However, since I do know the RGB of the hue and saturation (and original lightness), I thought that may help. Thank you for that graph– I actually was writing down values on a piece of paper, but that graph helps so much more.

            – GreatBlakes
            Jan 18 at 14:17











          • take a look at the color contrast checker on webaim.org/resources/contrastchecker. they have a lighten/darken slider that adjusts the color. it won't automatically bump it to a color that has sufficient contrast, but you can look at their code to see how they adjust the color lighter or darker. look at webaim.org/resources/contrastchecker/contrast.js. it's converting the rgb to hsl, which sounds like what you're doing. there must be a way to go backwards to generate a sufficient color.

            – slugolicious
            Jan 18 at 20:36











          • Wow- thanks so much for that update! I really appreciate your math help on this!

            – GreatBlakes
            Jan 21 at 15:34














          2












          2








          2







          So given #000000 and #444444, you can calculate the contrast ratio (2.15 in this case). The math is pretty straightforward, albeit a little hairy. (See the "relative luminance" definition.)



          Now you want to go backwards? If you have #000000 and want a ratio of 4.5, starting with #444444, what should the color be? Is that what




          I need to reverse my contrast formula




          means?



          It's a little complicated because you're solving for 3 variables, the red, green, and blue components, plus the luminance formula doesn't treat the red, green and blue equally. It's using 21.25% red, 71.5% green, and 7.25% blue.



          Plus, the luminance formula isn't a simple linear formula so you can't just take a percentage short of luminance and bump the color value by that same percentage.



          For example, in your case, the ratio was 2.15 but you need it to be 4.5. 2.15 is 108% short of 4.5, the desired value.



          However, if you look at your original RGB values #444444 and you calculated it needed to be #757575 (in order to have a 4.5 ratio), then if you treat those RGB values as simple numbers (and convert to decimal), then #444444 (4473924) is 72% short of #757575 (7697781).



          So you have a disconnect that your ratio is short by 108% but your RGB values are short by 72%. Thus you can't do a simple linear equation.



          (The numbers aren't quite exact since #757575 gives you a 4.56 ratio, not an exact 4.5 ratio. If you use #747474, you get a 4.49 ratio, which is just a smidge too small for WCAG compliance but is closer to 4.5 than 4.56. However, #444444 is 71% short of #747474, so it's still not the same as 2.15 being 108% short of 4.5, so the basic concept still applies.)



          Just for fun, I looked at the values of 0x11111 through 0x666666, incrementing by 0x111111, and calculated the contrast ratio. There weren't enough points on the graph so I added a color halfway between 0x111111 and 0x222222, then halfway between 0x222222 and 0x333333, etc.



          RGB     contrast  % from 4.5  % from 0x747474
          111111 1.11 305.41% 582.35%
          191919 1.19 278.15% 364.00%
          222222 1.32 240.91% 241.18%
          2a2a2a 1.46 208.22% 176.19%
          333333 1.66 171.08% 127.45%
          3b3b3b 1.87 140.64% 96.61%
          444444 2.16 108.33% 70.59%
          4c4c4c 2.45 83.67% 52.63%
          555555 2.82 59.57% 36.47%
          5d5d5d 3.19 41.07% 24.73%
          666666 3.66 22.95% 13.73%
          6e6e6e 4.12 9.22% 5.45%



          graph of contrast ratios



          As you can see, the lines interset at the 3rd data point then converge toward each other. I'm sure there's a formula in there so you could take the contrast percentage, do some (probably logarithmic) function on it and get the percentage needed to change the color.



          This would be a fascinating math problem that I currently don't have time to play with.



          Update Jan 18, 2019



          I got it to work going backwards, but it doesn't handle edge cases such as when making a dark color darker but you've already reached the max (or a light color lighter but you reached the max). But maybe you can play with it.



          Test case





          • #ee0add for the light color (magenta-ish)


          • #445566 for the dark color (dark gray)

          • contrast ratio 2.09


          When computing the "relative luminance" of a color, it has a conditional statement.



          if X <= 0.03928 then 
          X = X/12.92
          else
          X = ((X+0.055)/1.055) ^ 2.4


          Before X is used in that condition, it's divided by 255 to normalize the value between 0 and 1. So if you take the conditional value, 0.03928, and multiply by 255, you get 10.0164. Since RGB values must be integers, that means an RGB component of 10 (0x0A) or less will go through the "if" and anything 11 (0x0B) or bigger will go through the "else". So in my test case values, I wanted one of the color parts to be 10 (0x0A) (#EE0ADD).



          The relative luminance for #ee0add is 0.23614683378171950172526363525113 (0.236)



          The relative luminance for #445566 is 0.0868525191131797135799815832377 (0.0868)



          The "contrast ratio" is
          (0.236 + .05) / (0.0868 + .05) = 2.09



          (You can verify this ratio on https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=445566)



          If we want a ratio of 4.5, and we want #ee0add to not change, then we have to adjust #445566. That means you need to solve for:



          4.5 = (0.236 + .05) / (XX + .05)


          So the second luminance value (XX) needs to be 0.01358818528482655593894747450025 (0.0136)



          The original second luminance value was 0.0868525191131797135799815832377 (0.0868), so to get 0.01358818528482655593894747450025 (0.0136), we need to multiply the original by 0.15645125119651910313960717062698 (0.0136 / 0.0868 = 0.156) (or 15.6% of the original value)



          If we apply that 15.6% to each of the R, G, and B relative luminance values, and then work through the conditional statement above backwards, you can get the RGB values.



          Original luminance for #445566



          r = 0x44 = 68 
          g = 0x55 = 85
          b = 0x66 = 102

          r1 = 68 / 255 = 0.26666666666666666666666666666667
          g1 = 85 / 255 = 0.33333333333333333333333333333333
          b1 = 102 / 255 = 0.4

          r2 = ((.267 + .055) / 1.055)^^2.4 = 0.05780543019106721120703816752337
          g2 = ((.333 + .055) / 1.055)^^2.4 = 0.09084171118340767766490119106965
          b2 = ((.400 + .055) / 1.055)^^2.4 = 0.13286832155381791428570549818868

          l = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2
          = 0.0868525191131797135799815832377


          Working backwards, take 15.6% of the r2, g2, and b2 values



          r2 = 0.05780543019106721120703816752337 * 15.6% = 0.00904373187934550551617004082875
          g2 = 0.09084171118340767766490119106965 * 15.6% = 0.01421229937547695322310904970549
          b2 = 0.13286832155381791428570549818868 * 15.6% = 0.02078741515147623990363062978804


          Now undo that mess with ^^2.4 and the other stuff




          • to undo X^^2.4 you have to do the inverse, X^^(1/2.4) or X^^(0.4167)

          • then multiply by 1.055

          • then subtract 0.055

          • then multiply by 255


          pow( 0.00904373187934550551617004082875, 1/2.4) = 0.14075965680504652191078668676178
          pow( 0.01421229937547695322310904970549, 1/2.4) = 0.16993264267137740728089791717873
          pow( 0.02078741515147623990363062978804, 1/2.4) = 0.19910562853770829265100914759565


          multiply by 1.055



          0.14075965680504652191078668676178 * 1.055 = 0.14850143792932408061587995453368
          0.16993264267137740728089791717873 * 1.055 = 0.17927893801830316468134730262356
          0.19910562853770829265100914759565 * 1.055 = 0.21005643810728224874681465071341


          subtract 0.055



          0.14850143792932408061587995453368 - 0.055 = 0.09350143792932408061587995453368
          0.17927893801830316468134730262356 - 0.055 = 0.12427893801830316468134730262356
          0.21005643810728224874681465071341 - 0.055 = 0.15505643810728224874681465071341


          multiply by 255



          0.09350143792932408061587995453368 * 255 = 23.842866671977640557049388406088 = 24 = 0x18
          0.12427893801830316468134730262356 * 255 = 31.691129194667306993743562169008 = 32 = 0x20
          0.15505643810728224874681465071341 * 255 = 39.53939171735697343043773593192 = 40 = 0x28


          So the darker color is #182028. There are probably some rounding errors but if you check the original foreground color, #ee0add with the new color, #182028, you get a contrast ratio of 4.48. Just shy of 4.5, but like I said, probably some rounding errors.



          https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=182028



          I tried doing the same thing with #ee0add, keeping #445566 the same, but when going backwards and getting to the last step where you multiply by 255, I got numbers greater than 255, which are not valid RGB components (they can only go up to 0xFF). If I stopped the number at 255 then took the difference and added it to the smallest color value, I got a decent color but the ratio was 5.04, overshooting 4.5. I can post that math too if you want.






          share|improve this answer















          So given #000000 and #444444, you can calculate the contrast ratio (2.15 in this case). The math is pretty straightforward, albeit a little hairy. (See the "relative luminance" definition.)



          Now you want to go backwards? If you have #000000 and want a ratio of 4.5, starting with #444444, what should the color be? Is that what




          I need to reverse my contrast formula




          means?



          It's a little complicated because you're solving for 3 variables, the red, green, and blue components, plus the luminance formula doesn't treat the red, green and blue equally. It's using 21.25% red, 71.5% green, and 7.25% blue.



          Plus, the luminance formula isn't a simple linear formula so you can't just take a percentage short of luminance and bump the color value by that same percentage.



          For example, in your case, the ratio was 2.15 but you need it to be 4.5. 2.15 is 108% short of 4.5, the desired value.



          However, if you look at your original RGB values #444444 and you calculated it needed to be #757575 (in order to have a 4.5 ratio), then if you treat those RGB values as simple numbers (and convert to decimal), then #444444 (4473924) is 72% short of #757575 (7697781).



          So you have a disconnect that your ratio is short by 108% but your RGB values are short by 72%. Thus you can't do a simple linear equation.



          (The numbers aren't quite exact since #757575 gives you a 4.56 ratio, not an exact 4.5 ratio. If you use #747474, you get a 4.49 ratio, which is just a smidge too small for WCAG compliance but is closer to 4.5 than 4.56. However, #444444 is 71% short of #747474, so it's still not the same as 2.15 being 108% short of 4.5, so the basic concept still applies.)



          Just for fun, I looked at the values of 0x11111 through 0x666666, incrementing by 0x111111, and calculated the contrast ratio. There weren't enough points on the graph so I added a color halfway between 0x111111 and 0x222222, then halfway between 0x222222 and 0x333333, etc.



          RGB     contrast  % from 4.5  % from 0x747474
          111111 1.11 305.41% 582.35%
          191919 1.19 278.15% 364.00%
          222222 1.32 240.91% 241.18%
          2a2a2a 1.46 208.22% 176.19%
          333333 1.66 171.08% 127.45%
          3b3b3b 1.87 140.64% 96.61%
          444444 2.16 108.33% 70.59%
          4c4c4c 2.45 83.67% 52.63%
          555555 2.82 59.57% 36.47%
          5d5d5d 3.19 41.07% 24.73%
          666666 3.66 22.95% 13.73%
          6e6e6e 4.12 9.22% 5.45%



          graph of contrast ratios



          As you can see, the lines interset at the 3rd data point then converge toward each other. I'm sure there's a formula in there so you could take the contrast percentage, do some (probably logarithmic) function on it and get the percentage needed to change the color.



          This would be a fascinating math problem that I currently don't have time to play with.



          Update Jan 18, 2019



          I got it to work going backwards, but it doesn't handle edge cases such as when making a dark color darker but you've already reached the max (or a light color lighter but you reached the max). But maybe you can play with it.



          Test case





          • #ee0add for the light color (magenta-ish)


          • #445566 for the dark color (dark gray)

          • contrast ratio 2.09


          When computing the "relative luminance" of a color, it has a conditional statement.



          if X <= 0.03928 then 
          X = X/12.92
          else
          X = ((X+0.055)/1.055) ^ 2.4


          Before X is used in that condition, it's divided by 255 to normalize the value between 0 and 1. So if you take the conditional value, 0.03928, and multiply by 255, you get 10.0164. Since RGB values must be integers, that means an RGB component of 10 (0x0A) or less will go through the "if" and anything 11 (0x0B) or bigger will go through the "else". So in my test case values, I wanted one of the color parts to be 10 (0x0A) (#EE0ADD).



          The relative luminance for #ee0add is 0.23614683378171950172526363525113 (0.236)



          The relative luminance for #445566 is 0.0868525191131797135799815832377 (0.0868)



          The "contrast ratio" is
          (0.236 + .05) / (0.0868 + .05) = 2.09



          (You can verify this ratio on https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=445566)



          If we want a ratio of 4.5, and we want #ee0add to not change, then we have to adjust #445566. That means you need to solve for:



          4.5 = (0.236 + .05) / (XX + .05)


          So the second luminance value (XX) needs to be 0.01358818528482655593894747450025 (0.0136)



          The original second luminance value was 0.0868525191131797135799815832377 (0.0868), so to get 0.01358818528482655593894747450025 (0.0136), we need to multiply the original by 0.15645125119651910313960717062698 (0.0136 / 0.0868 = 0.156) (or 15.6% of the original value)



          If we apply that 15.6% to each of the R, G, and B relative luminance values, and then work through the conditional statement above backwards, you can get the RGB values.



          Original luminance for #445566



          r = 0x44 = 68 
          g = 0x55 = 85
          b = 0x66 = 102

          r1 = 68 / 255 = 0.26666666666666666666666666666667
          g1 = 85 / 255 = 0.33333333333333333333333333333333
          b1 = 102 / 255 = 0.4

          r2 = ((.267 + .055) / 1.055)^^2.4 = 0.05780543019106721120703816752337
          g2 = ((.333 + .055) / 1.055)^^2.4 = 0.09084171118340767766490119106965
          b2 = ((.400 + .055) / 1.055)^^2.4 = 0.13286832155381791428570549818868

          l = 0.2126 * r2 + 0.7152 * g2 + 0.0722 * b2
          = 0.0868525191131797135799815832377


          Working backwards, take 15.6% of the r2, g2, and b2 values



          r2 = 0.05780543019106721120703816752337 * 15.6% = 0.00904373187934550551617004082875
          g2 = 0.09084171118340767766490119106965 * 15.6% = 0.01421229937547695322310904970549
          b2 = 0.13286832155381791428570549818868 * 15.6% = 0.02078741515147623990363062978804


          Now undo that mess with ^^2.4 and the other stuff




          • to undo X^^2.4 you have to do the inverse, X^^(1/2.4) or X^^(0.4167)

          • then multiply by 1.055

          • then subtract 0.055

          • then multiply by 255


          pow( 0.00904373187934550551617004082875, 1/2.4) = 0.14075965680504652191078668676178
          pow( 0.01421229937547695322310904970549, 1/2.4) = 0.16993264267137740728089791717873
          pow( 0.02078741515147623990363062978804, 1/2.4) = 0.19910562853770829265100914759565


          multiply by 1.055



          0.14075965680504652191078668676178 * 1.055 = 0.14850143792932408061587995453368
          0.16993264267137740728089791717873 * 1.055 = 0.17927893801830316468134730262356
          0.19910562853770829265100914759565 * 1.055 = 0.21005643810728224874681465071341


          subtract 0.055



          0.14850143792932408061587995453368 - 0.055 = 0.09350143792932408061587995453368
          0.17927893801830316468134730262356 - 0.055 = 0.12427893801830316468134730262356
          0.21005643810728224874681465071341 - 0.055 = 0.15505643810728224874681465071341


          multiply by 255



          0.09350143792932408061587995453368 * 255 = 23.842866671977640557049388406088 = 24 = 0x18
          0.12427893801830316468134730262356 * 255 = 31.691129194667306993743562169008 = 32 = 0x20
          0.15505643810728224874681465071341 * 255 = 39.53939171735697343043773593192 = 40 = 0x28


          So the darker color is #182028. There are probably some rounding errors but if you check the original foreground color, #ee0add with the new color, #182028, you get a contrast ratio of 4.48. Just shy of 4.5, but like I said, probably some rounding errors.



          https://webaim.org/resources/contrastchecker/?fcolor=EE0ADD&bcolor=182028



          I tried doing the same thing with #ee0add, keeping #445566 the same, but when going backwards and getting to the last step where you multiply by 255, I got numbers greater than 255, which are not valid RGB components (they can only go up to 0xFF). If I stopped the number at 255 then took the difference and added it to the smallest color value, I got a decent color but the ratio was 5.04, overshooting 4.5. I can post that math too if you want.







          share|improve this answer














          share|improve this answer



          share|improve this answer








          edited Jan 19 at 2:09

























          answered Jan 18 at 3:44









          slugoliciousslugolicious

          4,80411318




          4,80411318













          • Thank you very much for your info/visuals. Yeah, I ran into some of the same things you were describing last night when trying to rework the formula. I knew that I could isolate L1 by multiplying L2 by the target contrast ratio and then I ran into the problem that you mentioned where I would then need to solve for red, green, and blue (in a non-equal way). However, since I do know the RGB of the hue and saturation (and original lightness), I thought that may help. Thank you for that graph– I actually was writing down values on a piece of paper, but that graph helps so much more.

            – GreatBlakes
            Jan 18 at 14:17











          • take a look at the color contrast checker on webaim.org/resources/contrastchecker. they have a lighten/darken slider that adjusts the color. it won't automatically bump it to a color that has sufficient contrast, but you can look at their code to see how they adjust the color lighter or darker. look at webaim.org/resources/contrastchecker/contrast.js. it's converting the rgb to hsl, which sounds like what you're doing. there must be a way to go backwards to generate a sufficient color.

            – slugolicious
            Jan 18 at 20:36











          • Wow- thanks so much for that update! I really appreciate your math help on this!

            – GreatBlakes
            Jan 21 at 15:34



















          • Thank you very much for your info/visuals. Yeah, I ran into some of the same things you were describing last night when trying to rework the formula. I knew that I could isolate L1 by multiplying L2 by the target contrast ratio and then I ran into the problem that you mentioned where I would then need to solve for red, green, and blue (in a non-equal way). However, since I do know the RGB of the hue and saturation (and original lightness), I thought that may help. Thank you for that graph– I actually was writing down values on a piece of paper, but that graph helps so much more.

            – GreatBlakes
            Jan 18 at 14:17











          • take a look at the color contrast checker on webaim.org/resources/contrastchecker. they have a lighten/darken slider that adjusts the color. it won't automatically bump it to a color that has sufficient contrast, but you can look at their code to see how they adjust the color lighter or darker. look at webaim.org/resources/contrastchecker/contrast.js. it's converting the rgb to hsl, which sounds like what you're doing. there must be a way to go backwards to generate a sufficient color.

            – slugolicious
            Jan 18 at 20:36











          • Wow- thanks so much for that update! I really appreciate your math help on this!

            – GreatBlakes
            Jan 21 at 15:34

















          Thank you very much for your info/visuals. Yeah, I ran into some of the same things you were describing last night when trying to rework the formula. I knew that I could isolate L1 by multiplying L2 by the target contrast ratio and then I ran into the problem that you mentioned where I would then need to solve for red, green, and blue (in a non-equal way). However, since I do know the RGB of the hue and saturation (and original lightness), I thought that may help. Thank you for that graph– I actually was writing down values on a piece of paper, but that graph helps so much more.

          – GreatBlakes
          Jan 18 at 14:17





          Thank you very much for your info/visuals. Yeah, I ran into some of the same things you were describing last night when trying to rework the formula. I knew that I could isolate L1 by multiplying L2 by the target contrast ratio and then I ran into the problem that you mentioned where I would then need to solve for red, green, and blue (in a non-equal way). However, since I do know the RGB of the hue and saturation (and original lightness), I thought that may help. Thank you for that graph– I actually was writing down values on a piece of paper, but that graph helps so much more.

          – GreatBlakes
          Jan 18 at 14:17













          take a look at the color contrast checker on webaim.org/resources/contrastchecker. they have a lighten/darken slider that adjusts the color. it won't automatically bump it to a color that has sufficient contrast, but you can look at their code to see how they adjust the color lighter or darker. look at webaim.org/resources/contrastchecker/contrast.js. it's converting the rgb to hsl, which sounds like what you're doing. there must be a way to go backwards to generate a sufficient color.

          – slugolicious
          Jan 18 at 20:36





          take a look at the color contrast checker on webaim.org/resources/contrastchecker. they have a lighten/darken slider that adjusts the color. it won't automatically bump it to a color that has sufficient contrast, but you can look at their code to see how they adjust the color lighter or darker. look at webaim.org/resources/contrastchecker/contrast.js. it's converting the rgb to hsl, which sounds like what you're doing. there must be a way to go backwards to generate a sufficient color.

          – slugolicious
          Jan 18 at 20:36













          Wow- thanks so much for that update! I really appreciate your math help on this!

          – GreatBlakes
          Jan 21 at 15:34





          Wow- thanks so much for that update! I really appreciate your math help on this!

          – GreatBlakes
          Jan 21 at 15:34


















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Stack Overflow!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f54241873%2fscaling-a-color-based-on-a-target-contrast-ratio%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown