<header>
  <h1>Gradients</h1>
  <p>In modern color spaces</p>
</header>

<form>
  <fieldset>
    <legend>Colors</legend>
    <input type="color" id="color1" value="#00ff00">
    <input type="color" id="color2" value="#0000FF">
  </fieldset>
  <fieldset>
    <legend>Steps</legend>
    <input type="range" id="quantize" min="1" max="100" value="10" />
  </fieldset>
  <fieldset>
    <legend>Hue Interpolation</legend>
    <label for="showInterpolations">Show shorter, longer, etc<br>
    <small>
      Only applies to cylindrical color spaces.<br>
      <code>shorter</code> is the default.</small></label> 
    <input type="checkbox" id="showInterpolations">
  </fieldset> 
</form>

<main id="app"></main>
@import "https://unpkg.com/open-props" layer(design.system);
@import "https://unpkg.com/open-props/normalize.min.css" layer(demo.support);
@import "https://unpkg.com/open-props/buttons.min.css" layer(demo.support);

@layer demo {
  main {
    display: grid;
    gap: var(--size-5);
    inline-size: clamp(90%, calc(var(--size-content-3) * 1.5), 90vw);
  }
  
  :has(#showInterpolations:not(:checked)) main {
    gap: 0;
  }
  
  fieldset {
    display: flex;
    align-items: center;
    gap: var(--size-3);
  }
  
  section {
    display: grid;
    grid-auto-flow: column;
    grid-template-columns: 20ch 1fr;
    align-items: center;
    gap: 2px var(--size-3);
  }
  
  @media (width < 500px) {
    section {
      grid-template-columns: 1fr;
    }
    
    section > h3 {
      grid-column: 1 / span 2;
    }
    
    section > .ramp {
      grid-column: -1/1;
    }
  }
  
  section > h3 {
    font-size: var(--font-size-4);
    text-transform: uppercase;
  }
  
  .cylindrical > h3 {
    grid-column: 1 / span 2;
  }
  
  section > p {
    grid-column: 1;
  }
  
  .ramp {
    grid-column: 2;
    display: flex;
    block-size: 5vh;
  }
  
  .swatch {
    flex: 1;
  }
  
  form {
    display: flex;
    flex-wrap: wrap;
    gap: var(--size-3);
    margin-block: var(--size-3) var(--size-8);
    justify-content: center;
  }
  
  header {
    text-align: center;
  }
}

@layer demo.support {
  body {
    display: grid;
    place-content: center;
    padding: var(--size-5);
    gap: var(--size-5);
    justify-items: center;
  } 
}
import Color from 'https://colorjs.io/dist/color.js'

const state = {
  quantize: quantize.value,
  showInterpolations: showInterpolations.checked,
  color: [color1.value, color2.value]
}

function updateUI() {
  const color1 = new Color(state.color[0])
  const color2 = new Color(state.color[1])
  
//   todo: linear gradients between steps?
  
  const spaces = ['srgb', 'srgb-linear', 'rec2020', 'p3', 'a98rgb', 'prophoto', 'xyz', 'lab', 'oklab', 'hsl', 'hwb', 'lch', 'oklch']
  const cylindrical = ['hsl', 'hwb', 'lch', 'oklch']
  const interpolations = ['shorter', 'longer', 'increasing', 'decreasing']
  
  let mix = null
  let rows = ''

  for (let space of spaces) {
    rows += `
        <section ${cylindrical.includes(space) && state.showInterpolations && `class="cylindrical"`}>
          <h3>${space}</h3>`
          
    if (cylindrical.includes(space) && state.showInterpolations) {
      for (let hue of interpolations) {
        mix = color1.steps(color2, {
          hue,
          space,
          steps: state.quantize,
          outputSpace: 'srgb',
        })

        rows += `
          <p>${hue}</p>
          <div class="ramp">
            ${mix.reduce((acc, item) => {
              return acc += `<div class="swatch" style="background: ${item}"></div>`
            }, '')}
          </div>
        `
      }
    }
    else {
      mix = color1.steps(color2, {
        space,
        steps: state.quantize,
        outputSpace: 'srgb',
      })
      
      rows += `
        <div class="ramp">
          ${mix.reduce((acc, item) => {
            return acc += `<div class="swatch" style="background: ${item}"></div>`
          }, '')}
        </div>
      `
    }
    
    rows += `</section>`
  }
  app.innerHTML = rows
}

color1.oninput = e => {
  state.color[0] = e.target.value
  updateUI()
}

color2.oninput = e => {
  state.color[1] = e.target.value
  updateUI()
}

quantize.oninput = e => {
  state.quantize = parseInt(e.target.value)
  updateUI()
}

showInterpolations.onchange = e => {
  state.showInterpolations = e.target.checked
  updateUI()
}

updateUI()
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.