Search code examples
javascripthtmlcsscss-selectorscss-variables

Make table cell background color based on input field inside it with CSS (not JS)


I know how to make a <td> cell background color reflect the value in the enclosed <input type=number> field using JavaScript (below is a snippet to do this). But is there a way to accomplish this without JavaScript? Using only CSS? I have full control over CSS, but not JavaScript. I can assume bleeding-edge browser versions for the latest CSS features.

function updateBgColor(td) {
    const input = td.getElementsByTagName('input')[0]
    td.style.backgroundColor = `rgb(96,${128+input.value*127},96)`
}

for (const input of document.getElementsByTagName('input')) {
    input.addEventListener('input', event => updateBgColor(event.target.parentElement))
}

[...document.getElementsByTagName('td')].forEach(updateBgColor)
input[type=number] {
  max-width: 6em;
  background-color: transparent;
}
<table>
  <tr>
    <td><input type=number value="0.9097174053213968"></td>
    <td><input type=number value="0.8408987079229189"></td>
    <td><input type=number value="0.7024472413860736"></td>
    <td><input type=number value="0.5185149759852405"></td>
  </tr>
  <tr>
    <td><input type=number value="0.51746948952139"></td>
    <td><input type=number value="0.23249116318958274"></td>
    <td><input type=number value="0.24954372649914003"></td>
    <td><input type=number value="0.7852880719259674"></td>
  </tr>
  <tr>
    <td><input type=number value="0.006423826937380195"></td>
    <td><input type=number value="0.18798660892180274"></td>
    <td><input type=number value="0.33787422244115817"></td>
    <td><input type=number value="0.2012761039221167"></td>
  </tr>
  <tr>
    <td><input type=number value="0.6635851618352506"></td>
    <td><input type=number value="0.4795899245078339"></td>
    <td><input type=number value="0.6457512930088372"></td>
    <td><input type=number value="0.6226533953736908"></td>
  </tr>
</table>


Solution

  • This is possible, but do be advised that it is (very) ugly, and onerous.

    To achieve the required result we can take advantage of the [attribute^="value"] notation, but this will – depending on the required precision – increase the size, and complexity, of your CSS. A simple example is below:

    // simple utility functions to reduce typing, and simplify the use of
    // Array methods with NodeLists, by converting them to Arrays:
    const D = document,
      create = (tag, props) => Object.assign(D.createElement(tag), props),
      fragment = () => D.createDocumentFragment(),
      get = (selector, context = D) => context.querySelector(selector),
      getAll = (selector, context = D) => [...context.querySelectorAll(selector)];
    
    /*
      function below taken from Matthias OTT's post:
        https://matthiasott.com/notes/detecting-css-selector-support-with-javascript
    */
    const isSelectorSupported = (selector) => {
      try {
        document.querySelector(selector)
        return true
      } catch (error) {
        return false
      }
    };
    
    let h = 0,
      s = 60,
      l = 65;
    
    // here we set the text of the relevant element to represent the browser's claims of
    // support for the :has() selector:
    get('aside span.claimsSupport').textContent = isSelectorSupported('td:has(input[value="0.5"])') ? 'yes' : 'no';
    
    // we retrieve the <button> element, and bind the anonymous function as the
    // event-handler for the 'click' event:
    get('button').addEventListener('click',
      (e) => {
    
        // retrieving the <span> element within the <button> (the e.currentTarget node):
        let toggleSwitch = get('span[data-use]', e.currentTarget);
        // updating its dataset.use property/data-use attribute; switchin between
        // 'JavaScript' and 'CSS', to indicate what the <button> does/will do:
        toggleSwitch.dataset.use = toggleSwitch.dataset.use === 'JavaScript' ? 'CSS' : 'JavaScript';
    
    
        // retrieving the <table> element:
        let table = get('table');
    
        // using the Element.classList API to toggle the 'js' class on/off:
        table.classList.toggle('js');
    
        // if the <table> currently has the 'js' class:
        if (table.classList.contains('js')) {
          // we create an (empty) Array of 10, using Array.from():
          Array.from({
            length: 10
            // iterate over that created Array using Array.prototype.forEach():
          }).forEach(
            // pass the current Array index to the function body:
            (_, i) => {
              // determine whether the index is odd or even:
              let isOdd = i % 2 === 0,
                // and use a template literal to construct a hsl() color string, with
                // CSS color level 4 syntax; the use of 'isOdd' and conditional operators
                // is to provide a little more variation between colors; though this
                // would have been better in an Array of colors, probably:
                hsl = `hsl( ${i * 36}deg ${s + (isOdd ? 10 : -10)}% ${l + (isOdd ? -10 : 10)}% / 1)`;
    
              // we then get the elements that match the selector, so <td> elements that
              // have <input> elements whose value attribute starts with a string of
              // "0.i" where "i" is the current index:
              getAll(`td:has(input[value^="0.${i}"`).forEach(
                // we then iterate over that Array of elements, and pass a reference
                // to the current element ('el') to the function body, and update its
                // background-color to the string we created:
                (el) => el.style.backgroundColor = hsl
              );
            });
          // if the <table> does not have the 'js' class:
        } else {
          // we remove the <style> attribute to remove the JS-generated colors:
          getAll('td', table).forEach((el) => el.removeAttribute('style'));
        }
    
      });
    
    /*
     */
    /* CSS custom properties used to share certain property-values across
       multiple elements in the document: */
    :root {
      --inputInlineSize: 6rem;
      --spacing: 0.55rem;
    }
    
    /* simple reset, to remove default margins and padding, and to ensure
       that all elements are sized to include their borders and padding within
       their declared sizes: */
    *,
    ::before,
    ::after {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      min-block-size: 100vh;
      padding: var(--spacing);
    }
    
    main {
      /* the default color of the variable is below, this is used within
         the background-image (next): */
      --resultColor: lightpink;
      /* using a linear gradient to visually indicate whether the
         current browser supports the use of :has() along with
         attribute-selectors; the default indicates false: */
      background-image: linear-gradient(to bottom right, lightskyblue, transparent, var(--resultColor));
      border: 2px solid currentColor;
      inline-size: clamp(40rem, 80%, 1200px);
      margin-inline: auto;
      padding: var(--spacing);
    }
    
    section {
      text-align: center;
    }
    
    button {
      line-height: 1.5;
      display: inline-block;
      margin-block: var(--spacing);
      padding-block: var(--spacing);
      padding-inline: var(--spacing);
    }
    
    button::first-letter {
      text-transform: uppercase;
    }
    
    button span::after {
      content: attr(data-use);
    }
    
    table {
      border-collapse: collapse;
      margin-inline: auto;
    }
    
    td {
      border: 1px solid currentColor;
      padding-block: min(0.5rem, var(--spacing));
      padding-inline: var(--spacing);
    }
    
    /* the following rules are responsible for styling the background-color
       of the <td> elements; here we select all <td> elements that are not
       the descendant of a <table class="js"> element, and which contain
       an <input> with a value starting with "0.n", where "n" is an integer
       in the range of 0-9: */
    table:not(.js) td:has(input[value^="0.0"]) {
      background-color: hsl(0deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.1"]) {
      background-color: hsl(36deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.2"]) {
      background-color: hsl(72deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.3"]) {
      background-color: hsl(108deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.4"]) {
      background-color: hsl(144deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.5"]) {
      background-color: hsl(180deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.6"]) {
      background-color: hsl(216deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.7"]) {
      background-color: hsl(252deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.8"]) {
      background-color: hsl(288deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.9"]) {
      background-color: hsl(324deg 50% 75% / 1);
    }
    
    input {
      inline-size: var(--inputInlineSize);
      padding-block: 0.2rem;
      text-indent: var(--spacing);
    }
    
    /* here we're testing the browser to see if it claims
       support of the selector written within the
       selector() function: */
    @supports selector(td:has(input[value^="0.1"])) {
      /* if so, we then update the --resultColor property-value,
         which is reflected in the <main> element's background
         image; do remember that the browser claiming support
         does not mean that the browser has any guaranteed level
         of support for the given selector: */
      main {
        --resultColor: palegreen;
      }
    }
    <main>
      <aside>
        <p>Does your browser claim to support <code>:has()</code> selector: <span class="claimsSupport"></span></p>
        <p>If, after pressing the &lt;button&gt; below, there is no change to the &lt;table&gt; presentation, then it probably does; otherwise, it may still be unsupported, partially supported, badly supported, or behind a flag.</p>
      </aside>
      <section>
        <button>colour &lt;table&gt; with <span data-use="JavaScript"></span></button>
        <table>
          <tbody>
            <tr>
              <td><input type="number" value="0.91"></td>
              <td><input type="number" value="0.84"></td>
              <td><input type="number" value="0.70"></td>
              <td><input type="number" value="0.52"></td>
            </tr>
            <tr>
              <td><input type="number" value="0.52"></td>
              <td><input type="number" value="0.23"></td>
              <td><input type="number" value="0.25"></td>
              <td><input type="number" value="0.79"></td>
            </tr>
            <tr>
              <td><input type="number" value="0.01"></td>
              <td><input type="number" value="0.19"></td>
              <td><input type="number" value="0.34"></td>
              <td><input type="number" value="0.20"></td>
            </tr>
            <tr>
              <td><input type="number" value="0.66"></td>
              <td><input type="number" value="0.48"></td>
              <td><input type="number" value="0.65"></td>
              <td><input type="number" value="0.62"></td>
            </tr>
          </tbody>
        </table>
      </section>
    </main>

    JS Fiddle demo.

    With reference to the CSS required to style a cell based on a decimal value to one single digit, below:

    table:not(.js) td:has(input[value^="0.0"]) {
      background-color: hsl(0deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.1"]) {
      background-color: hsl(36deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.2"]) {
      background-color: hsl(72deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.3"]) {
      background-color: hsl(108deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.4"]) {
      background-color: hsl(144deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.5"]) {
      background-color: hsl(180deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.6"]) {
      background-color: hsl(216deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.7"]) {
      background-color: hsl(252deg 50% 75% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.8"]) {
      background-color: hsl(288deg 70% 55% / 1);
    }
    
    table:not(.js) td:has(input[value^="0.9"]) {
      background-color: hsl(324deg 50% 75% / 1);
    }
    

    We have ten rulesets, if you wish to extend that to two decimal places – to include both tenths and hundredths – that will require a hundred rules. To move beyond that, to three decimal places – tenths, hundredths, and thousandths – would require a thousand rules.

    With every degree of precision we're increasing the number of rules to the power n, where n is the number of decimal places required. This would lead to a very large CSS stylesheet. Ultimately, while this is possible, it's important to remember the – paraphrased – words of the illustrious, though sadly fictional, Dr. Ian Malcolm:

    Just because you can do something, doesn't mean you should do something.

    References: