<main>
  <div class="target js-target" style="--hue: 280; --saturation: 80%; --lightness: 80%;">      
    <button class="change-button js-change-color">Change Color</button>
  </div>
  
  <div class="pickers">
    <section class="rgb-picker color-picker">
      <header aria-live="polite" aria-atomic="true">
        <h2>Hexadecimal Picker</h2>
        <span><span class="percent">0</span>% Match</span>
      </header>
      <div class="picker-and-output">
        <form>
          <label for="r">
            Red:

            <input type="range" value="122.5" min="0" max="255" step="0.1" name="r" id="r">
          </label>

          <label for="s">
            Green:

            <input type="range" value="122.5" min="0" max="255" step="0.1" name="g" id="g">
          </label>

          <label for="l">
            Blue:

            <input type="range" value="122.5" min="0" max="255" step="0.1" name="b" id="b">
          </label>
        </form>
        <div class="output">
          <code class="output-text">
            #7a7a7aff 
          </code>
        </div>
      </div>
    </section>

    <section class="hsl-picker color-picker">
      <header aria-live="polite" aria-atomic="true">
        <h2>HSL Picker</h2>
        <span><span class="percent">0</span>% Match</span>
      </header>      
      <div class="picker-and-output">
        <form>
          <label for="h">
            Hue:

            <input type="range" value="180" min="0" max="360" step="0.1" name="h" id="h">
          </label>

          <label for="s">
            Saturation:

            <input type="range" value="50" min="0" max="100" step="0.1" name="s" id="s">
          </label>

          <label for="l">
            Lightness:

            <input type="range" value="50" min="0" max="100" step="0.1" name="l" id="l">
          </label>
        </form>
        <div class="output">
          <code class="output-text">
            hsl(180, 50%, 50%)
          </code>
        </div>
      </div>
    </section>
  </div>
</main>
/**
 * General Rules
 */

html, body {
  margin: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif; 
}

* {
  font-family: inherit;
}

/** 
 * Target color section styles
 */

.target {
  padding: 1em;
  min-height: 6em;
  background-color: hsl(
    var(--hue),
    var(--saturation),
    var(--lightness)
  );
  align-items: center;
  justify-content: center;
  display: flex;
  flex-grow: 1;
}

/**
 * Layout
 */

.pickers {
  display: flex;
  flex-wrap: wrap;
  gap: 0.2em;
}

h2 {
  margin: 0;
  font-size: 1.2em;
}

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.8em;
}

header span {
  font-weight: bold;
}

/**
 * Color pickers
 */

.color-picker {
  background: #eee;
  padding: 1em;
  flex-grow: 1;
  width: min(50%, 15em);
}

.picker-and-output {
  display: flex;
}

.rgb-picker {
  --r: 122.5;
  --g: 122.5;
  --b: 122.5;
  --a: 1;
  
  --border-color: rgba(var(--r), var(--g), var(--b), var(--a));
}

.hsl-picker {
  --h: 180;
  --s: 50%;
  --l: 50%;
  --a: 100%;
  
  --border-color: hsl(var(--h), 100%, 35%);
}

/**
 * Color picker layout stuff
 */

.color-picker form {
  flex-grow: 1;
  margin-right: 2em;
}

.color-picker label {
  display: flex;
  flex-direction: column;
  width: 100%;
}

.color-picker label + label {
  margin-top: 1em;
}

/**
 * Color picker output color + text
 */

.output {
  flex-basis: min(20em, 40%);
  display: flex;
  align-items: flex-end;
  position: relative;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3E%3Cg fill='%23000000' fill-opacity='0.4'%3E%3Cpath fill-rule='evenodd' d='M0 0h4v4H0V0zm4 4h4v4H4V4z'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 20px 20px;
  background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}

.output::before {
  content: '';
  top: 0;
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
}

.rgb-picker .output::before{
  background-color: rgba(var(--r), var(--g), var(--b), var(--a));
}

.hsl-picker .output::before {
  background-color: hsla(var(--h), var(--s), var(--l), var(--a));
}

.output-text {
  font-family: monospace;
  margin: 0.5em;
  padding: 0.5em;
  background: hsla(0, 0%, 95%, 95%);
  position: relative;
  flex-grow: 1;
  text-align: center;
}

/**
 * General range input styles
 */

input[type="range"] {
  margin-top: 1em;
  width: 100%;
  -webkit-appearance: none;
  height: 1em;
  box-shadow: 0 0 0 0.12em #fff;
  border-radius: 1em;
}

input[type="range"]:focus {
  outline: none;
}

.rgb-picker input[type="range"]:focus {
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em rgb(var(--r), var(--g), var(--b));
}

.hsl-picker input[type="range"]:focus {
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em hsl(var(--h), 50%, 50%);
}

/**
 * Individual range slider styles
 * Using custom props for dynamic color updates
 */

#r {
  background: linear-gradient(
    to right, 
    rgb(0, var(--g), var(--b)) 0%,     
    rgb(255, var(--g), var(--b)) 100% 
  );
}

#g {
  background: linear-gradient(
    to right, 
    rgb(var(--r), 0, var(--b)) 0%,     
    rgb(var(--r), 255, var(--b)) 100% 
  );
}

#b {
   background: linear-gradient(
     to right, 
     rgb(var(--r), var(--g), 0) 0%,     
     rgb(var(--r), var(--g), 255) 100% 
   ); 
}

#h {
  --s: 100%;
  --l: 50%;
  /* credit to Jamie Kudla: https://codepen.io/JKudla/pen/GpYXxZ */
  background: linear-gradient(
    to right, 
    hsl(0, 100%, 50%) 0%, 
    hsl(30, 100%, 50%) 8.3%, 
    hsl(60, 100%, 50%) 16.6%, 
    hsl(90, 100%, 50%) 25%, 
    hsl(120, 100%, 50%) 33.3%, 
    hsl(150, 100%, 50%) 41.6%, 
    hsl(180, 100%, 50%) 50%, 
    hsl(210, 100%, 50%) 58.3%, 
    hsl(240, 100%, 50%) 66.6%, 
    hsl(270, 100%, 50%) 75%, 
    hsl(300, 100%, 50%) 83.3%, 
    hsl(330, 100%, 50%) 91.6%, 
    hsl(360, 100%, 50%) 100%
  );
}

#s {
  background: linear-gradient(
    to right, 
    hsl(var(--h), 0%, var(--l)) 0%, 
    hsl(var(--h), 100%, var(--l)) 100%
  );
}

#l {
   background: linear-gradient(
     to right, 
     hsl(var(--h), var(--s), 0%) 0%, 
     hsl(var(--h), var(--s), 50%) 50%, 
     hsl(var(--h), var(--s), 100%) 100%
   ); 
}

/**
 * Thumb styles
 * Unfortunately these need to be re-set for every browser
 */

[type="range"]::-moz-range-thumb {
  border-radius: 50%;
  height: 1.5em;
  width: 1.5em;
}

.rgb-picker [type="range"]::-moz-range-thumb {
  background-color: rgb(var(--r), var(--g), var(--b));
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em rgb(var(--r), var(--g), var(--b));
  border: none;
}

.hsl-picker [type="range"]::-moz-range-thumb {
  background-color: hsl(var(--h), var(--s), var(--l));
  border: 0.1em solid hsl(var(--h), 100%, 35%);
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em hsl(var(--h), 100%, 35%);
}

[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  border-radius: 50%;
  height: 1.5em;
  width: 1.5em;
}

.rgb-picker [type="range"]::-webkit-slider-thumb {
  background-color: rgb(var(--r), var(--g), var(--b));
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em rgb(var(--r), var(--g), var(--b));
  border: none;
}

.hsl-picker [type="range"]::-webkit-slider-thumb {
  background-color: hsl(var(--h), var(--s), var(--l));
  border: 0.1em solid hsl(var(--h), 100%, 35%);
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em hsl(var(--h), 100%, 35%);
}

[type="range"]::-ms-thumb {
  border-radius: 50%;
  height: 1.5em;
  width: 1.5em;
}

.rgb-picker [type="range"]::-ms-thumb {
  background-color: rgb(var(--r), var(--g), var(--b));
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em rgb(var(--r), var(--g), var(--b));
  border: none;
}

.hsl-picker [type="range"]::-ms-thumb {
  background-color: hsl(var(--h), var(--s), var(--l));
  border: 0.1em solid hsl(var(--h), 100%, 35%);
  box-shadow: 
    0 0 0 0.2em #fff,
    0 0 0 0.4em hsl(var(--h), 100%, 35%);
}

/**
 * Button Stuff
 */

button {
  /* Text Colors */
  --text-saturation: 90%;
  --text-lightness: 30%;
  --text-color-hover: hsl(
    var(--hue),
    calc(var(--text-saturation) + 10%),
    calc(var(--text-lightness) - 5%)
  );
  --text-color-active: hsl(
    var(--hue),
    calc(var(--text-saturation) + 10%),
    calc(var(--text-lightness) - 10%)
  );
  
--text-color-disabled: hsl(
    var(--hue),
    calc(var(--text-saturation) - 60%),
    calc(var(--text-lightness) + 10%)
  );
  
  /* Background Colors */
  --background-saturation: 0%;
  --background-lightness: 100%;
  --background-color-hover: hsl(
    var(--hue),
    calc(var(--background-saturation) + 80%),
    calc(var(--background-lightness) - 5%)
  );
  --background-color-active: hsl(
    var(--hue),
    calc(var(--background-saturation) + 80%),
    calc(var(--background-lightness) - 10%)
  );
  --background-color-disabled: hsl(
    var(--hue),
    calc(var(--background-saturation) + 30%),
    calc(var(--background-lightness) - 10%)
  );
  
  /* Border Colors */
  --border-saturation: 90%;
  --border-lightness: 60%;
  --border-color-hover: hsl(
    var(--hue),
    calc(var(--border-saturation) + 10%),
    calc(var(--border-lightness) - 10%)
  );
  --border-color-active: hsl(
    var(--hue),
    calc(var(--border-saturation) + 10%),
    calc(var(--border-lightness) - 20%)
  );
  --border-color-disabled: hsl(
    var(--hue),
    calc(var(--border-saturation) - 60%),
    calc(var(--border-lightness) + 20%)
  );
  
  --shadow-color-focus: hsl(
    var(--hue),
    100%,
    85%
  );
  
  /* Color Styles */
  color: hsl(var(--hue), var(--text-saturation), var(--text-lightness));
  background-color: hsl(var(--hue), var(--background-saturation), var(--background-lightness)); 
  border:0.1em solid hsl(var(--hue), var(--border-saturation), var(--border-lightness)); 
  
  /* Misc. Styles */
  border-radius: 0.25em;
  cursor: pointer;
  display: inline-block;
  font-size: 1em;
  padding: 0.5em 1em;
  transition-property: box-shadow, background-color, border-color, color;
  transition-timing-function: ease-out;
  transition-duration: 0.2s;
}

button:hover {
  color: var(--text-color-hover);
  background-color: var(--background-color-hover);
  border-color: var(--border-color-hover);
}

button:active {
  color: var(--text-color-active);
  background-color: var(--background-color-active);
  border-color: var(--border-color-active);
}

button:focus {
  outline: none;
  box-shadow: 0 0 0 0.25em var(--shadow-color-focus);
}

button[disabled] {
  color: var(--text-color-disabled);
  background-color: var(--background-color-disabled);
  border-color: var(--border-color-disabled);
}
/**
 * RGB Picker + HSL picker could be DRYed up 
 */

/**
 * RGB Picker
 */

initRgbPicker(document.querySelector('.rgb-picker'));

function initRgbPicker(picker) {
  const outputText = picker.querySelector('.output-text');
  
  picker.querySelectorAll('[type="range"]').forEach(input => {
    input.addEventListener('input', ({target}) => {
      let {name, value} = target;

      picker.style.setProperty(`--${name}`, value);
      
      const styles = window.getComputedStyle(picker);
      
      outputText.innerText = `
        #${
          parseInt(styles.getPropertyValue('--r')).toString(16)
        }${
          parseInt(styles.getPropertyValue('--g')).toString(16)
        }${
          parseInt(styles.getPropertyValue('--b')).toString(16)
        }${
          parseInt(styles.getPropertyValue('--a') * 255).toString(16)
        }
      `.trim();
      
      updateContrastPercentages({rgb: true});
    });
  });
}

/**
 * HSL Picker
 */

initHslPicker(document.querySelector('.hsl-picker'));

function initHslPicker(picker) {
  const outputText = picker.querySelector('.output-text');
  
  picker.querySelectorAll('[type="range"]').forEach(input => {
    input.addEventListener('input', ({target}) => {
      let {name, value} = target;

      if(target.name !== 'h') {
        value = `${value}%`;
      }

      picker.style.setProperty(`--${name}`, value);
      
      const styles = window.getComputedStyle(picker);
      
      outputText.innerText = `
        hsl(${
          styles.getPropertyValue('--h')
        }, ${
          styles.getPropertyValue('--s')
        }, ${
          styles.getPropertyValue('--l')
        })
      `.trim();
      
      updateContrastPercentages({hsl: true});
    });
  });
}

/**
 * Our target color
 */

initTargetColor();

function initTargetColor() {
  const button = document.querySelector('.js-change-color');
  const target = document.querySelector('.js-target');
  
  button.addEventListener('click', () => {
    target.style.setProperty('--hue', Math.random() * 360);
    target.style.setProperty('--saturation', `${Math.random() * 100}%`);
    target.style.setProperty('--lightness', `${Math.random() * 100}%`);
    updateContrastPercentages({hsl: true, rgb: true});
  });
}

/**
 * Logic to update our % matches whenever anything changes 
 */

updateContrastPercentages({hsl: true, rgb: true});

function updateContrastPercentages({hsl = false, rgb = false}) {
  const target = document.querySelector('.js-target');
  const targetStyles = window.getComputedStyle(target);
  const targetColor = hslToRgb(
    targetStyles.getPropertyValue('--hue').trim(),
    targetStyles.getPropertyValue('--saturation').replace('%', '').trim(),
    targetStyles.getPropertyValue('--lightness').replace('%', '').trim()
  ) 
  
  if(hsl) {
    const hslPicker = document.querySelector('.hsl-picker');
    const hslStyles = window.getComputedStyle(hslPicker);
    const hslColor = hslToRgb(
      hslStyles.getPropertyValue('--h').trim(),
      hslStyles.getPropertyValue('--s').replace('%', '').trim(),
      hslStyles.getPropertyValue('--l').replace('%', '').trim()
    );
    
    hslPicker.querySelector('.percent').textContent = getMatchPercentage(targetColor, hslColor);
  }
  
  if(rgb) {
    const hexPicker = document.querySelector('.rgb-picker');
    const hexStyles = window.getComputedStyle(hexPicker);
    const hexColor = [
      hexStyles.getPropertyValue('--r'),
      hexStyles.getPropertyValue('--g'),
      hexStyles.getPropertyValue('--b')
    ];
    
    hexPicker.querySelector('.percent').textContent = getMatchPercentage(targetColor, hexColor);
  }
}

// https://stackoverflow.com/a/36722579/7816145
/**
     * Converts an HSL color value to RGB. Conversion formula
     * adapted from http://en.wikipedia.org/wiki/HSL_color_space.
     * Assumes h, s, and l are contained in the set [0, 1] and
     * returns r, g, and b in the set [0, 255].
     *
     * @param   {number}  h       The hue
     * @param   {number}  s       The saturation
     * @param   {number}  l       The lightness
     * @return  {Array}           The RGB representation
     */
function hslToRgb(h, s, l){
  h = h / 360;
  s = s / 100;
  l = l / 100;
  var r, g, b;

  if(s == 0){
    r = g = b = l; // achromatic
  }else{
    var hue2rgb = function hue2rgb(p, q, t){
      if(t < 0) t += 1;
      if(t > 1) t -= 1;
      if(t < 1/6) return p + (q - p) * 6 * t;
      if(t < 1/2) return q;
      if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
      return p;
    }

    var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    var p = 2 * l - q;
    r = hue2rgb(p, q, h + 1/3);
    g = hue2rgb(p, q, h);
    b = hue2rgb(p, q, h - 1/3);
  }

  return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}

/**
 * Format our match into our final display
 */

function getMatchPercentage(rgbA, rgbB) {
  const match = 100 - deltaE(rgbA, rgbB)
  if(match < 0) match = 0;
  return Math.round(match * 10) / 10;
}

/**
 * Get Euclidean distance between two colors
 */
function deltaE(rgbA, rgbB) {
  let labA = rgb2lab(rgbA);
  let labB = rgb2lab(rgbB);
  let deltaL = labA[0] - labB[0];
  let deltaA = labA[1] - labB[1];
  let deltaB = labA[2] - labB[2];
  let c1 = Math.sqrt(labA[1] * labA[1] + labA[2] * labA[2]);
  let c2 = Math.sqrt(labB[1] * labB[1] + labB[2] * labB[2]);
  let deltaC = c1 - c2;
  let deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
  deltaH = deltaH < 0 ? 0 : Math.sqrt(deltaH);
  let sc = 1.0 + 0.045 * c1;
  let sh = 1.0 + 0.015 * c1;
  let deltaLKlsl = deltaL / (1.0);
  let deltaCkcsc = deltaC / (sc);
  let deltaHkhsh = deltaH / (sh);
  let i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc + deltaHkhsh * deltaHkhsh;
  return i < 0 ? 0 : Math.sqrt(i);
}

/**
 * To compute the color contrast we need to convert to LAB colors
 */

function rgb2lab(rgb){
  let r = rgb[0] / 255, g = rgb[1] / 255, b = rgb[2] / 255, x, y, z;
  r = (r > 0.04045) ? Math.pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
  g = (g > 0.04045) ? Math.pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
  b = (b > 0.04045) ? Math.pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
  x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
  y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
  z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
  x = (x > 0.008856) ? Math.pow(x, 1/3) : (7.787 * x) + 16/116;
  y = (y > 0.008856) ? Math.pow(y, 1/3) : (7.787 * y) + 16/116;
  z = (z > 0.008856) ? Math.pow(z, 1/3) : (7.787 * z) + 16/116;
  return [(116 * y) - 16, 500 * (x - y), 200 * (y - z)]
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.