Mischback / colorizer

A simple web-based colorscheme builder which focuses on contrast values.
https://mischback.github.io/colorizer/
MIT License
1 stars 0 forks source link

Improve Input of Colors #8

Closed Mischback closed 1 year ago

Mischback commented 1 year ago

As of now, colors have to be added using hex notation in a text-based input field. This works well enough but is not really convenient.

Requirements

Resources

Mischback commented 1 year ago

Color Input by Sliders

Providing sliders - or inputs of type range - is a convenient way to adjust the colors, especially for the hue components of HSL, HWB and OkLCH.

Providing a meaningful interface to these sliders is not that easy and requires lots of code, in JS aswell as CSS.

Proof of Concept

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Colorizer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="assets/style.css">
    <script src="assets/colorizer.js" defer></script>
    <style>
#this-hue-container {
  --this-hue: 0;
  --this-sat: 100%;
  --this-light: 50%;
}

.hue {
  --slider-track-height: 1em;
  --slider-thumb-height: 1.5em;
  --slider-thumb-width: 1em;

  width: 25%;
  padding: 2rem;
}

.hue input[type=range] {
  -webkit-appearance: none;
  width: 100%;
  background: transparent;
}

.hue input[type=range]::-webkit-slider-runnable-track {
  width: 100%;
  height: var(--slider-track-height);
  cursor: pointer;
  border-radius: 2px;
  border: 1px solid #000;
  background: linear-gradient(90deg,
    hsl(0 var(--this-sat) var(--this-light)) 0%, 
    hsl(calc(0.1 * 360) var(--this-sat) var(--this-light)) 10%, 
    hsl(calc(0.2 * 360) var(--this-sat) var(--this-light)) 20%, 
    hsl(calc(0.3 * 360) var(--this-sat) var(--this-light)) 30%,
    hsl(calc(0.4 * 360) var(--this-sat) var(--this-light)) 40%,
    hsl(calc(0.5 * 360) var(--this-sat) var(--this-light)) 50%,
    hsl(calc(0.6 * 360) var(--this-sat) var(--this-light)) 60%,
    hsl(calc(0.7 * 360) var(--this-sat) var(--this-light)) 70%,
    hsl(calc(0.8 * 360) var(--this-sat) var(--this-light)) 80%,
    hsl(calc(0.9 * 360) var(--this-sat) var(--this-light)) 90%,
    hsl(360 var(--this-sat) var(--this-light)) 100%);
}

.hue input[type=range]::-moz-range-track {
  width: 100%;
  height: var(--slider-track-height);
  cursor: pointer;
  border-radius: 2px;
  border: 1px solid #000;
  background: linear-gradient(90deg,
    hsl(0 var(--this-sat) var(--this-light)) 0%, 
    hsl(calc(0.1 * 360) var(--this-sat) var(--this-light)) 10%, 
    hsl(calc(0.2 * 360) var(--this-sat) var(--this-light)) 20%, 
    hsl(calc(0.3 * 360) var(--this-sat) var(--this-light)) 30%,
    hsl(calc(0.4 * 360) var(--this-sat) var(--this-light)) 40%,
    hsl(calc(0.5 * 360) var(--this-sat) var(--this-light)) 50%,
    hsl(calc(0.6 * 360) var(--this-sat) var(--this-light)) 60%,
    hsl(calc(0.7 * 360) var(--this-sat) var(--this-light)) 70%,
    hsl(calc(0.8 * 360) var(--this-sat) var(--this-light)) 80%,
    hsl(calc(0.9 * 360) var(--this-sat) var(--this-light)) 90%,
    hsl(360 var(--this-sat) var(--this-light)) 100%);
}

.hue input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  height: var(--slider-thumb-height);
  width: var(--slider-thumb-width);
  border: 1px solid #000;
  background: hsl(var(--this-hue) var(--this-sat) var(--this-light));
  cursor: pointer;
  margin-top: calc((var(--slider-thumb-height) - var(--slider-track-height)) / -2);
}

.hue input[type=range]::-moz-range-thumb {
  -webkit-appearance: none;
  height: var(--slider-thumb-height);
  width: var(--slider-thumb-width);
  border: 1px solid #000;
  background: hsl(var(--this-hue) var(--this-sat) var(--this-light));
  cursor: pointer;
  /* FIXME: This might be obsolete for FF or might let it break. */
  margin-top: calc((var(--slider-thumb-height) - var(--slider-track-height)) / -2);
}
    </style>
    <script type="text/javascript">
document.addEventListener("DOMContentLoaded", (e) => {
  console.info("DOM ready, manipulating the slider");

  const hueContainer = document.querySelector("#this-hue-container");
  const hueSlider = document.querySelector("#this-hue-container input[type=range]");
  const hueValue = document.querySelector("#this-hue-container .value");

  hueSlider.addEventListener("input", (e) => {
    console.log(`Slider value: ${hueSlider.value}`);
    hueValue.innerHTML = hueSlider.value;
    hueContainer.style.setProperty("--this-hue", Number(hueSlider.value));
  });
});
    </script>
  </head>

  <body>
    <header>
      <h1>Colorizer</h1>
      <p>A tool to build color palettes while considering contrast values.</p>
    </header>

<form>
  <div class="hue" id="this-hue-container">
    <input type="range" min="0" max="360" step="0.01">
    <span class="value">0.00</span>
  </div>
</form>

  </body>
</html>

Issues

Resources

Mischback commented 1 year ago

Experimental 01

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Colorizer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript">
/**
 * Attach tooltip to a toggle button.
 *
 * @param fsId The ID of the fieldset
 *
 * The implementation is based on this guide
 * https://inclusive-components.design/tooltips-toggletips/
 * but makes some assumptions about the general structure of the DOM.
 */
function setupColorFormFieldsetTooltip(fsId) {
  // Get the required DOM elements
  //
  // - ``ttButton`` is the actual control element. Clicking the button will show
  //   the tooltip.
  // - ``ttContent`` is the container of the actual content of the tooltip. Its
  //   ``innerHTML`` will be placed in ``ttDisplay``.
  // - ``ttDisplay`` is the actual tooltip. All styling should relate to this
  //   element.
  let ttButton = document.querySelector(`#${fsId} > legend > .tooltip-anchor > button`);
  let ttContent = document.querySelector(`#${fsId}-tooltip`);
  let ttDisplay = document.querySelector(`#${fsId} > legend > .tooltip-anchor > .tooltip-display`);

  // Attach the required event listeners

  // Show the tooltip on click of the button
  ttButton.addEventListener("click", () => {
    ttDisplay.innerHTML = "";
    window.setTimeout(() => {
      ttDisplay.innerHTML = `<div>${ttContent.innerHTML}</div>`;
    }, 100);  // TODO: Should this timeout be adjustable?
  });

  // Hide the tooltip when ESC is pressed
  ttButton.addEventListener("keydown", (e) => {
    if ((e.keyCode || e.which) === 27)
      ttDisplay.innerHTML = "";
  });

  // Hide the tooltip when the button loses focus, e.g. the user clicks
  // somewhere else
  document.addEventListener("click", (e) => {
    if (e.target !== ttButton)
      ttDisplay.innerHTML = "";
  });

  // Setup is completed, now remove ``ttContent`` from the (visual) DOM
  //
  // TODO: Is this the correct way? Or should it be *removed* from the DOM by
  //       using ``visibility: hidden``?
  ttContent.classList.add("hide-visually");
}

function devGenericCallback(data) {
  console.log(`R: ${data.r}, G: ${data.g}, B: ${data.b}`);
}

function devGenericCallback2(r, g, b) {
  console.log(`R2: ${r}, G2: ${g}, B2: ${b}`);
}

/**
 * Establish the logical connections between slider, text input and the containing fieldset.
 *
 * @param container DOM element, this input method's overall ``<fieldset>``.
 * @param slider DOM element, the ``<input type="range" ...>`` element.
 * @param text DOM element, the ``<input type="text" ...>`` element.
 * @param property ``string`` The CSS custom property to make the value accessible for CSS.
 *
 * Adds event listeners for ``input`` events to the ``<input ...>`` elements to
 * update the corresponding *other* ``<input ...>`` element.
 * The ``container`` element's ``style`` attribute is updated with a CSS
 * custom property, providing the value of the ``<input ...>`` elements for
 * styling purposes.
 */
function linkContainerSliderText(container, slider, text, property) {
  slider.addEventListener("input", () => {
    let val = Number(slider.value);
    text.value = val;
    container.style.setProperty(property, val);
  });
  text.addEventListener("input", () => {
    let val = Number(text.value);
    slider.value = val;
    container.style.setProperty(property, val);
  });
}

function debounceInput(fn, d) {
  let timer;
  return function () {
    clearTimeout(timer);
    timer = setTimeout(fn, d);
  }
}

function setupColorFormInputRgb(cbFunc) {
  const fsId = "color-form-rgb";
  let inputRed, inputGreen, inputBlue, inputSlider;

  function cbWrapper() {
    cbFunc({r: inputRed.value, g: inputGreen.value, b: inputBlue.value});
  }

  let methodFs = document.querySelector(`#${fsId}`);

  inputRed = document.querySelector(`#${fsId} .component-red > input[type=text]`);
  inputSlider = document.querySelector(`#${fsId} .component-red > input[type=range]`);
  linkContainerSliderText(methodFs, inputSlider, inputRed, "--this-red");
  inputRed.addEventListener("input", debounceInput(cbWrapper, 500));
  inputSlider.addEventListener("input", debounceInput(cbWrapper, 500));

  inputGreen = document.querySelector(`#${fsId} .component-green > input[type=text]`);
  inputSlider = document.querySelector(`#${fsId} .component-green > input[type=range]`);
  linkContainerSliderText(methodFs, inputSlider, inputGreen, "--this-green");
  inputGreen.addEventListener("input", debounceInput(cbWrapper, 500));
  inputSlider.addEventListener("input", debounceInput(cbWrapper, 500));

  inputBlue = document.querySelector(`#${fsId} .component-blue > input[type=text]`);
  inputSlider = document.querySelector(`#${fsId} .component-blue > input[type=range]`);
  linkContainerSliderText(methodFs, inputSlider, inputBlue, "--this-blue");
  inputBlue.addEventListener("input", debounceInput(cbWrapper, 500));
  inputSlider.addEventListener("input", debounceInput(cbWrapper, 500));

  setupColorFormFieldsetTooltip(fsId);
}

document.addEventListener("DOMContentLoaded", () => {
  console.info("DOM ready, doing stuff!");

  setupColorFormInputRgb(devGenericCallback);
});
    </script>
    <style>
* {
  box-sizing: border-box;
}

/* This hides the element visually, but keep it as part of the DOM for
 * accessibility.
 * Ref: https://www.w3.org/WAI/tutorials/forms/labels/#note-on-hiding-elements
 */
.hide-visually {
  position: absolute;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  overflow: hidden;
}

/* Place and style the tooltips, which are provided as part of an input 
 * method's ``<legend>``.
 */
.color-form-input-method > legend > .tooltip-anchor {
  position: relative;
  display: inline-block;
}
.color-form-input-method > legend > .tooltip-anchor button {
  font-size: 0.9em;
  font-weight: bold;
  border-radius: 5em;
  border: 1px solid black;
}
.color-form-input-method > legend > .tooltip-anchor > .tooltip-display > div {
  position: absolute;
  display: block;
  top: 0;
  left: 100%;

  font-size: 1rem;
  width: 20em;
  padding: 0.25em 0.5em;
  background: rgb(200 200 200);
  color: black;
  border: 1px solid black;
}

#color-form {
  --input-text-size: 1.25em;
  --input-text-vertical-padding: 0.25em;
  --input-text-border-width: 1px;
}

.color-form-input-method > fieldset {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
}

.color-form-input-method > fieldset > input {
  font-size: var(--input-text-size);
  margin: 0;
  padding: var(--input-text-vertical-padding) 0.5em;
  font-family: monospace;
  border: var(--input-text-border-width) solid #000;
}

.color-form-input-method > fieldset > input[type=range] {
  --slider-track-height: 1em;
  --slider-thumb-height: calc(var(--slider-track-height) * 1.5);
  --slider-thumb-width: var(--slider-thumb-height);

  -webkit-appearance: none;
  width: 10em;
  border: 0;
  background: transparent;
}
.color-form-input-method > fieldset > input[type=range]::-webkit-slider-runnable-track {
  width: 100%;
  height: 1em;
  cursor: pointer;
  border-radius: 5px;
  border: 1px solid #000;
  background: rgb(200 200 200);
}
.color-form-input-method > fieldset > input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  height: var(--slider-thumb-height);
  width: var(--slider-thumb-width);
  border: 1px solid #000;
  border-radius: 999px;
  cursor: pointer;
  margin-top: calc((var(--slider-thumb-height) - var(--slider-track-height)) / -2);
}

#color-form-rgb {
  --this-red: 0;
  --this-green: 0;
  --this-blue: 0;
}
#color-form-rgb > .component-red > input[type=range]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg,
    rgb(0 var(--this-green) var(--this-blue)) 0%,
    rgb(255 var(--this-green) var(--this-blue)) 100%);
}
#color-form-rgb > .component-green > input[type=range]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg,
    rgb(var(--this-red) 0 var(--this-blue)) 0%,
    rgb(var(--this-red) 255 var(--this-blue)) 100%);
}
#color-form-rgb > .component-blue > input[type=range]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg,
    rgb(var(--this-red) var(--this-green) 0) 0%,
    rgb(var(--this-red) var(--this-green) 255) 100%);
}
#color-form-rgb > fieldset > input[type=range]::-webkit-slider-thumb {
  background: rgb(var(--this-red) var(--this-green) var(--this-blue));
}
    </style>
  </head>

  <body>
    <header>
      <h1>Colorizer</h1>
      <p>A tool to build color palettes while considering contrast values.</p>
    </header>

<form id="color-form">
  <fieldset id="color-form-rgb" class="color-form-input-method">
    <legend>
      <strong>RGB input</strong>
      <span class="tooltip-anchor">
        <button type="button" aria-label="Description of the RGB input">?</button>
        <div class="tooltip-display" role="status"></div>
      </span>
    </legend>
    <fieldset class="component-red">
      <legend>Red component</legend>
      <label for="color-form-rgb-r-slider" class="hide-visually">Slider to adjust red component</label>
      <input id="color-form-rgb-r-slider" type="range" min="0" max="255" step="1" aria-describedby="color-form-rgb-tooltip">
      <label for="color-form-rgb-r" class="hide-visually">Decimal input for red component</label>
      <input id="color-form-rgb-r" type="text" inputmode="numeric" aria-describedby="color-form-rgb-tooltip">
    </fieldset>
    <fieldset class="component-green">
      <legend>Green component</legend>
      <label for="color-form-rgb-g-slider" class="hide-visually">Slider to adjust green component</label>
      <input id="color-form-rgb-g-slider" type="range" min="0" max="255" step="1" aria-describedby="color-form-rgb-tooltip">
      <label for="color-form-rgb-g" class="hide-visually">Decimal input for green component</label>
      <input id="color-form-rgb-g" type="text" inputmode="numeric" aria-describedby="color-form-rgb-tooltip">
    </fieldset>
    <fieldset class="component-blue">
      <legend>Blue component</legend>
      <label for="color-form-rgb-b-slider" class="hide-visually">Slider to adjust blue component</label>
      <input id="color-form-rgb-b-slider" type="range" min="0" max="255" step="1" aria-describedby="color-form-rgb-tooltip">
      <label for="color-form-rgb-b" class="hide-visually">Decimal input for blue component</label>
      <input id="color-form-rgb-b" type="text" inputmode="numeric" aria-describedby="color-form-rgb-tooltip">
    </fieldset>
    <div role="tooltip" id="color-form-rgb-tooltip">
      <p>The RGB input lets you specify the values for R, G and B components of the color directly.</p>
      <p>Each of the components may be specified in a range between 0 and 255.</p>
      <p>The values may be entered directly or using the slider.</p>
    </div>
  </fieldset>
</form>

  </body>
</html>
Mischback commented 1 year ago

Experimental 02

This is a re-implementation of Experimental 01, with a clear and class-based interface to the form - or more specifically - the fieldset that provides RGB input.

This is the preferred approach. Differences between 01/02 are only in the JS part, HTML and CSS are untouched!

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Colorizer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <script type="text/javascript">

class ColorFormInput {
  // TODO: Can this be a static method? Should this be a static method?
  _getDomElement(query) {
    // console.debug(`ColorFormInput.#getDomElement() query: ${query}`);

    let tmp = document.querySelector(query);
    if (tmp === null) {
      throw new Error(`Missing required DOM element with query '${query}'`);
    }

    return tmp;
  }

  /**
   * Establish the logical connections between slider, text input and the containing fieldset.
   *
   * @param container DOM element, this input method's overall ``<fieldset>``.
   * @param slider DOM element, the ``<input type="range" ...>`` element.
   * @param text DOM element, the ``<input type="text" ...>`` element.
   * @param property ``string`` The CSS custom property to make the value accessible for CSS.
   *
   * Adds event listeners for ``input`` events to the ``<input ...>`` elements to
   * update the corresponding *other* ``<input ...>`` element.
   * The ``container`` element's ``style`` attribute is updated with a CSS
   * custom property, providing the value of the ``<input ...>`` elements for
   * styling purposes.
   */
  // TODO: Can this be a static method? Should this be a static method?
  _linkContainerSliderText(container, slider, text, property) {
    slider.addEventListener("input", () => {
      let val = Number(slider.value);
      text.value = val;
      container.style.setProperty(property, val);
    });
    text.addEventListener("input", () => {
      let val = Number(text.value);
      slider.value = val;
      container.style.setProperty(property, val);
    });
  }

  /**
   * Attach tooltip to a toggle button.
   *
   * @param fsId The ID of the fieldset
   *
   * The implementation is based on this guide
   * https://inclusive-components.design/tooltips-toggletips/
   * but makes some assumptions about the general structure of the DOM.
   */
  // TODO: Can this be a static method? Should this be a static method?
  _setupColorFormFieldsetTooltip(fsId) {
    // Get the required DOM elements
    //
    // - ``ttButton`` is the actual control element. Clicking the button will show
    //   the tooltip.
    // - ``ttContent`` is the container of the actual content of the tooltip. Its
    //   ``innerHTML`` will be placed in ``ttDisplay``.
    // - ``ttDisplay`` is the actual tooltip. All styling should relate to this
    //   element.
    let ttButton = this._getDomElement(`#${fsId} > legend > .tooltip-anchor > button`);
    let ttContent = this._getDomElement(`#${fsId}-tooltip`);
    let ttDisplay = this._getDomElement(`#${fsId} > legend > .tooltip-anchor > .tooltip-display`);

    // Attach the required event listeners

    // Show the tooltip on click of the button
    ttButton.addEventListener("click", () => {
      ttDisplay.innerHTML = "";
      window.setTimeout(() => {
        ttDisplay.innerHTML = `<div>${ttContent.innerHTML}</div>`;
      }, 100);  // TODO: Should this timeout be adjustable?
    });

    // Hide the tooltip when ESC is pressed
    ttButton.addEventListener("keydown", (e) => {
      if ((e.keyCode || e.which) === 27)
        ttDisplay.innerHTML = "";
    });

    // Hide the tooltip when the button loses focus, e.g. the user clicks
    // somewhere else
    document.addEventListener("click", (e) => {
      if (e.target !== ttButton)
        ttDisplay.innerHTML = "";
    });

    // Setup is completed, now remove ``ttContent`` from the (visual) DOM
    //
    // TODO: Is this the correct way? Or should it be *removed* from the DOM by
    //       using ``visibility: hidden``?
    ttContent.classList.add("hide-visually");
  }

  /**
   * 
   *
   * Reference: https://chiamakaikeanyi.dev/event-debouncing-and-throttling-in-javascript/
   */
  // TODO: Can this be a static method? Should this be a static method?
  _debounce(context, fn, debounceTime) {
    let timer;

    return (...args) => {
      clearTimeout(timer);
      timer = setTimeout(() => {
        fn.apply(context, args);
      }, debounceTime);
    }
  }

  _publishColor(ev) {
    console.log(ev);
  }
}

class ColorFormInputRgb extends ColorFormInput {

  // Private Attributes
  #fieldset;
  #inputTextRed;
  #inputSliderRed;
  #inputTextGreen;
  #inputSliderGreen;
  #inputTextBlue;
  #inputSliderBlue;

  static #fieldsetId = "color-form-rgb";

  constructor() {
    super();

    // Get DOM elements
    this.#fieldset = this._getDomElement(`#${this.constructor.#fieldsetId}`);
    this.#inputTextRed = this._getDomElement(`#${this.constructor.#fieldsetId} .component-red > input[type=text]`);
    this.#inputSliderRed = this._getDomElement(`#${this.constructor.#fieldsetId} .component-red > input[type=range]`);
    this.#inputTextGreen = this._getDomElement(`#${this.constructor.#fieldsetId} .component-green > input[type=text]`);
    this.#inputSliderGreen = this._getDomElement(`#${this.constructor.#fieldsetId} .component-green > input[type=range]`);
    this.#inputTextBlue = this._getDomElement(`#${this.constructor.#fieldsetId} .component-blue > input[type=text]`);
    this.#inputSliderBlue = this._getDomElement(`#${this.constructor.#fieldsetId} .component-blue > input[type=range]`);

    // Establish connections between related input elements
    this._linkContainerSliderText(this.#fieldset, this.#inputSliderRed, this.#inputTextRed, "--this-red");
    this._linkContainerSliderText(this.#fieldset, this.#inputSliderGreen, this.#inputTextGreen, "--this-green");
    this._linkContainerSliderText(this.#fieldset, this.#inputSliderBlue, this.#inputTextBlue, "--this-blue");

    // Attach event listeners
    this.#inputTextRed.addEventListener("input", this._debounce(this, this._publishColor, 500));
    this.#inputSliderRed.addEventListener("input", this._debounce(this, this._publishColor, 500));
    this.#inputTextGreen.addEventListener("input", this._debounce(this, this._publishColor, 500));
    this.#inputSliderGreen.addEventListener("input", this._debounce(this, this._publishColor, 500));
    this.#inputTextBlue.addEventListener("input", this._debounce(this, this._publishColor, 500));
    this.#inputSliderBlue.addEventListener("input", this._debounce(this, this._publishColor, 500));

    this._setupColorFormFieldsetTooltip(this.constructor.#fieldsetId);
  }

  _publishColor(ev) {
    // console.debug(ev);
    // FIXME: Probably a good place to check the validity of the inputs!
    console.info(`R: ${this.#inputTextRed.value}, G: ${this.#inputTextGreen.value}, B: ${this.#inputTextBlue.value}`);
  }
}

document.addEventListener("DOMContentLoaded", () => {
  console.info("DOM ready, doing stuff!");

  const foo = new ColorFormInputRgb();
});
    </script>
    <style>
* {
  box-sizing: border-box;
}

/* This hides the element visually, but keep it as part of the DOM for
 * accessibility.
 * Ref: https://www.w3.org/WAI/tutorials/forms/labels/#note-on-hiding-elements
 */
.hide-visually {
  position: absolute;
  margin: -1px;
  padding: 0;
  border: 0;
  clip: rect(0 0 0 0);
  height: 1px;
  width: 1px;
  overflow: hidden;
}

/* Place and style the tooltips, which are provided as part of an input 
 * method's ``<legend>``.
 */
.color-form-input-method > legend > .tooltip-anchor {
  position: relative;
  display: inline-block;
}
.color-form-input-method > legend > .tooltip-anchor button {
  font-size: 0.9em;
  font-weight: bold;
  border-radius: 5em;
  border: 1px solid black;
}
.color-form-input-method > legend > .tooltip-anchor > .tooltip-display > div {
  position: absolute;
  display: block;
  top: 0;
  left: 100%;

  font-size: 1rem;
  width: 20em;
  padding: 0.25em 0.5em;
  background: rgb(200 200 200);
  color: black;
  border: 1px solid black;
}

#color-form {
  --input-text-size: 1.25em;
  --input-text-vertical-padding: 0.25em;
  --input-text-border-width: 1px;
}

.color-form-input-method > fieldset {
  display: flex;
  flex-direction: row;
  justify-content: flex-start;
}

.color-form-input-method > fieldset > input {
  font-size: var(--input-text-size);
  margin: 0;
  padding: var(--input-text-vertical-padding) 0.5em;
  font-family: monospace;
  border: var(--input-text-border-width) solid #000;
}

.color-form-input-method > fieldset > input[type=range] {
  --slider-track-height: 1em;
  --slider-thumb-height: calc(var(--slider-track-height) * 1.5);
  --slider-thumb-width: var(--slider-thumb-height);

  -webkit-appearance: none;
  width: 10em;
  border: 0;
  background: transparent;
}
.color-form-input-method > fieldset > input[type=range]::-webkit-slider-runnable-track {
  width: 100%;
  height: 1em;
  cursor: pointer;
  border-radius: 5px;
  border: 1px solid #000;
  background: rgb(200 200 200);
}
.color-form-input-method > fieldset > input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  height: var(--slider-thumb-height);
  width: var(--slider-thumb-width);
  border: 1px solid #000;
  border-radius: 999px;
  cursor: pointer;
  margin-top: calc((var(--slider-thumb-height) - var(--slider-track-height)) / -2);
}

#color-form-rgb {
  --this-red: 0;
  --this-green: 0;
  --this-blue: 0;
}
#color-form-rgb > .component-red > input[type=range]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg,
    rgb(0 var(--this-green) var(--this-blue)) 0%,
    rgb(255 var(--this-green) var(--this-blue)) 100%);
}
#color-form-rgb > .component-green > input[type=range]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg,
    rgb(var(--this-red) 0 var(--this-blue)) 0%,
    rgb(var(--this-red) 255 var(--this-blue)) 100%);
}
#color-form-rgb > .component-blue > input[type=range]::-webkit-slider-runnable-track {
  background: linear-gradient(90deg,
    rgb(var(--this-red) var(--this-green) 0) 0%,
    rgb(var(--this-red) var(--this-green) 255) 100%);
}
#color-form-rgb > fieldset > input[type=range]::-webkit-slider-thumb {
  background: rgb(var(--this-red) var(--this-green) var(--this-blue));
}
    </style>
  </head>

  <body>
    <header>
      <h1>Colorizer</h1>
      <p>A tool to build color palettes while considering contrast values.</p>
    </header>

<form id="color-form">
  <fieldset id="color-form-rgb" class="color-form-input-method">
    <legend>
      <strong>RGB input</strong>
      <span class="tooltip-anchor">
        <button type="button" aria-label="Description of the RGB input">?</button>
        <div class="tooltip-display" role="status"></div>
      </span>
    </legend>
    <fieldset class="component-red">
      <legend>Red component</legend>
      <label for="color-form-rgb-r-slider" class="hide-visually">Slider to adjust red component</label>
      <input id="color-form-rgb-r-slider" type="range" min="0" max="255" step="1" aria-describedby="color-form-rgb-tooltip">
      <label for="color-form-rgb-r" class="hide-visually">Decimal input for red component</label>
      <input id="color-form-rgb-r" type="text" inputmode="numeric" aria-describedby="color-form-rgb-tooltip">
    </fieldset>
    <fieldset class="component-green">
      <legend>Green component</legend>
      <label for="color-form-rgb-g-slider" class="hide-visually">Slider to adjust green component</label>
      <input id="color-form-rgb-g-slider" type="range" min="0" max="255" step="1" aria-describedby="color-form-rgb-tooltip">
      <label for="color-form-rgb-g" class="hide-visually">Decimal input for green component</label>
      <input id="color-form-rgb-g" type="text" inputmode="numeric" aria-describedby="color-form-rgb-tooltip">
    </fieldset>
    <fieldset class="component-blue">
      <legend>Blue component</legend>
      <label for="color-form-rgb-b-slider" class="hide-visually">Slider to adjust blue component</label>
      <input id="color-form-rgb-b-slider" type="range" min="0" max="255" step="1" aria-describedby="color-form-rgb-tooltip">
      <label for="color-form-rgb-b" class="hide-visually">Decimal input for blue component</label>
      <input id="color-form-rgb-b" type="text" inputmode="numeric" aria-describedby="color-form-rgb-tooltip">
    </fieldset>
    <div role="tooltip" id="color-form-rgb-tooltip">
      <p>The RGB input lets you specify the values for R, G and B components of the color directly.</p>
      <p>Each of the components may be specified in a range between 0 and 255.</p>
      <p>The values may be entered directly or using the slider.</p>
    </div>
  </fieldset>
</form>

  </body>
</html>

Resources