<form>
  <h1>Derive colors from user input at runtime with pre-compiled LESS mixins.</h1>
  <label>
    Pick the primary color (used for the background):
    <input name="primary" type="color" value="#bbfff0"></input>
  </label>
  <label>
    Pick the secondary color:
    <input name="secondary" type="color" value="#fe78fb"></input>
  </label>
  <div>I'm the primary color in a dark variant.</div>
</form>
@color-primary: .importHslFromGlobalColorVar('primary')[];
@color-secondary: .importHslFromGlobalColorVar('secondary')[];
@color-contrast: .contrast(@color-primary, 20)[];
@color-primary-shadow: .darken(@color-primary, 30)[];

body {
  font-size: 3vw;
  font-family: system, -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif;  
  
  background: .hsl(@color-primary)[];
  color: .hsl(@color-contrast)[];  
}

form {
  display: flex;
  flex-direction: column;
  
  div {
    color: .hsl(@color-primary-shadow)[];
  }
  
  div, &::after {
    text-shadow: .hsla(@color-contrast, .7)[] 1px 1px 2px;
  }
  
  &::after {
    content: "I'm the secondary color.";
    color: .hsl(@color-secondary)[];
  }
  
  &::before {
    order: 1;
    content: "I'm primary and secondary blended equally, on a contrasting background.";
     
    @blended: .blend(@color-primary, @color-secondary, 50)[];
    color: .hsl(@blended)[];
    background: .hsl(.contrast(@blended, 100)[])[];
    
    padding: .5em 0;
    margin-top: .2em;
  }
}


// Manipulation functions for user specific colors
// ---------------------------------------------------
// Internally, colors used in this context use the HSL colorspace, and are serialized
// into a kind of JS object string, e.g.: "{ h: 90, s: 100, l: 50 }" for chartreuse.
//
// Because of this, and because we're using LESS mixins as functions,
// when declaring and/or passing around color variables of this kind,
// you'll have to append `[]`,as explained in
// http://lesscss.org/features/#mixins-feature-unnamed-lookups, like this:
//
//   @bgcol: "{ h: 90, s: 100, l: 50 }"; // chartreuse
//   @bg-darker: .darken(@bgcol, 10)[];
//
// When using colors created by these mixins in CSS properties, you'll have to convert them
// into proper CSS format by using the .hsl() mixin (there's also a .hsla() mixin available
// so you can transparentize your color in the last step), again followed by `[]`, like this:
//
//   body { background: linear-gradient(to right, .hsl(@bgcol)[], .hsl(@bg-darker)[]) }
//   h1 { color: .hsl(.contrast(@bgcol, 80)[])[] }
//   h2 { text-shadow: .hsla(@bg-darker, .5)[] 1px 1px 2px }
//
//
// In practice, these color manipulation functions only make sense for colors unknown
// at compile time, that is colors derived from user input at run time.
// They'll have to be provided to the according CSS scope as custom properties,
// with one property each for the three HSL channels. To import them into a LESS variable,
// use the according mixin, like so:
//
//   // HTML document, rendered at runtime
//   :root { --colors-background-h: 90; --colors-background-s: 100; --colors-background-l: 50 }
//   // LESS file
//   @bgcol: .importHslFromGlobalColorVar('background')[];

.importHslFromGlobalColorVar(@varName) {
  @serialized: "{ h: var(--colors-@{varName}-h), s: var(--colors-@{varName}-s), l: var(--colors-@{varName}-l) }";
}

.deserialize(@color) {
  @regex: "{ h: (.*?), s: (.*?), l: (.*?) }";
  @h: replace(@color, @regex, "$1");
  @s: replace(@color, @regex, "$2");
  @l: replace(@color, @regex, "$3");
}

.hsl(@color) {
  .deserialize(@color);
  @hsl: ~"hsl(@{h}, calc(@{s} * 1%), calc(@{l} * 1%))";
}

.hsla(@color, @alpha) {
  .deserialize(@color);
  @hsla: ~"hsla(@{h}, calc(@{s} * 1%), calc(@{l} * 1%), @{alpha})";
}

.lighten(@color, @amount) {
  .deserialize(@color);
  @lNew: ~"calc(@{l} + @{amount})";
  @serialized: ~"{ h: @{h}, s: @{s}, l: @{lNew} }";
}

.darken(@color, @amount) {
  .deserialize(@color);
  @lNew: ~"calc(@{l} - @{amount})";
  @serialized: ~"{ h: @{h}, s: @{s}, l: @{lNew} }";
}

// for an explanation of the formula, see https://codepen.io/depoulo/pen/WLGeQz
.contrast(@color, @ratio) {
  .deserialize(@color);
  @lNew: ~"calc((-2500 * (@{ratio} / 100 + 1)) / (@{l} - 49.999) + @{l})";
  @serialized: ~"{ h: @{h}, s: @{s}, l: @{lNew} }";
}

// for a demo, see https://codepen.io/depoulo/pen/oJPyad
.blend(@color1, @color2, @amount) {
  @color1-h: .deserialize(@color1) [ @h];
  @color1-s: .deserialize(@color1) [ @s];
  @color1-l: .deserialize(@color1) [ @l];
  @color2-h: .deserialize(@color2) [ @h];
  @color2-s: .deserialize(@color2) [ @s];
  @color2-l: .deserialize(@color2) [ @l];
  @hNew: ~"calc((@{color1-h} * ((100 - @{amount}) * 0.01)) + (@{color2-h} * (@{amount} * 0.01)))";
  @sNew: ~"calc((@{color1-s} * ((100 - @{amount}) * 0.01)) + (@{color2-s} * (@{amount} * 0.01)))";
  @lNew: ~"calc((@{color1-l} * ((100 - @{amount}) * 0.01)) + (@{color2-l} * (@{amount} * 0.01)))";
  @serialized: ~"{ h: @{hNew}, s: @{sNew}, l: @{lNew} }";
}
Array.from(document.querySelectorAll('input')).forEach(input => updateStyle(input))
document.addEventListener('input', ({ target }) => requestAnimationFrame(() => updateStyle(target)))

function updateStyle(element) {
  Object.entries(
    rgbToHsl(...hexToRgb(element.value))
  ).forEach(([prop, value]) => {
    document.body.style.setProperty(
      `--colors-${element.name}-${prop}`,
      String(value.toFixed(1)).replace(/\.0+$/, '')
    )
  })
}

function hexToRgb(hex) {
  return hex
    .split('')
    .reduce((rgb, val, idx) => rgb + (idx % 2 ? val : val + ','), '')
    .split(',')
    .splice(1, 3)
    .map(val => parseInt(val, 16))
}

function rgbToHsl(r, g, b) {
  r /= 255, g /= 255, b /= 255
  const max = Math.max(r, g, b), min = Math.min(r, g, b)
  let h, s, l = (max + min) / 2

  if(max == min){
    h = s = 0; // achromatic
  } else {
    let d = max - min
    s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
    h = ({
      [r]: (g - b) / d,
      [g]: 2 + ( (b - r) / d),
      [b]: 4 + ( (r - g) / d),
    })[max] * 60
    if (h < 0) h +=360
  }
  s *= 100
  l *= 100
  return { h, s, l }
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.