skip to Main Content

I’m building a text tracking (letter-spacing) demo and I have range sliders updating CSS props but I’d like to extend it so that the labels also update dynamically. It’s sort of working but hacky. Is there a way to do it elegantly, so I’m not repeating output2.textContent = this.value; output2.textContent = this.value; etc.?

Also, the UI janks when the values go from (eg) 0.09 -> 0.1 -> 0.11. Can I have it always output two decimals? Script is before the close.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Trax…text tracking tool</title>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Chivo&family=Chivo+Mono&family=Playfair+Display+SC:wght@700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
    <meta name="description" content="description">
    <style>
        * {
            outline: cornflowerblue dotted 0.5px;
        }

        * {
            margin: 0;
        }

        :root {
            --_trax-fontFamily-caps: 'Playfair Display', serif;
            --_trax-fontFamily-scaps: 'Playfair Display SC', serif;
            --_trax-fontFamily-body: 'Chivo', sans-serif;
            --_trax-fontFamily-mono: 'Chivo Mono', monospace;

            --_trax-textTracking-caps: var(--trax-textTracking-caps, 0.00cap);
            --_trax-textTracking-scaps: var(--trax-textTracking-scaps, 0.00ex);
            --_trax-textTracking-body: var(--trax-textTracking-body, 0.00em);
            --_trax-textTracking-mono: var(--trax-textTracking-mono, 0.00ch);

            --_trax-text-measure: var(--trax-text-measure, 66em);
        }

        body {
            box-sizing: content-box;
            margin-inline: auto;
            text-align: center;
            max-inline-size: var(--_trax-text-measure);
            padding-inline-start: 1rem;
            padding-inline-end: 1rem;
            display: flex;
            flex-direction: column;
            align-items: center;
            font-family: var(--_trax-fontFamily-mono);
        }

        h1 {
            font-size: 5rem;
        }

        h1.caps {
            font-family: var(--_trax-fontFamily-caps);
            letter-spacing: var(--_trax-textTracking-caps);
            text-transform: uppercase;
        }

        h1.scaps {
            font-family: var(--_trax-fontFamily-scaps);
            letter-spacing: var(--_trax-textTracking-scaps);
        }

        .align-self:flex-start,
        .asfs {
            align-self: flex-start;
        }

        /* add range slider code */
    </style>
</head>

<body>

    <p class="align-self:flex-start">Capitals</p>
    <h1 class="caps">Hamburgevons</h1>
    <div class="controls">
        <label for="trax-textTracking-caps" id="value-caps">0.00</label><span> cap</span>
        <input name="trax-textTracking-caps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="cap">
        <button>CSS</button>
    </div>

    <p class="align-self:flex-start">Small Capitals</p>
    <h1 class="scaps">Hamburgevons</h1>
    <div class="controls">
        <label for="trax-textTracking-scaps" id="value-scaps">0.00</label><span> ex</span>
        <input name="trax-textTracking-scaps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="ex">
        <button>CSS</button>
    </div>

    <script>
        const rangeSliders = document.querySelectorAll('input');
        //const labels = document.querySelectorAll('label');
        const output1 = document.querySelector('#value-caps');
        const output2 = document.querySelector('#value-scaps');

        function updateProp(event) {
            const uom = this.dataset.uom || '';
            document.documentElement.style.setProperty(`--_${this.name}`, this.value + uom);
            output1.textContent = this.value;
            output2.textContent = this.value;
        }

        rangeSliders.forEach(input => {
            input.addEventListener('input', updateProp);
        });
    </script>

</body>

</html>

4

Answers


  1. To update only the relevant <label> element of the <input> element being updated, you could use DOM tree querying from the <input> element (the this or event.target in updateProp()). For example, both labels are 2nd-previous element siblings, so you could reference them "relatively" with:

    this.previousElementSibling.previousElementSibling.textContent = this.value
    

    To avoid "UI jank" when going between 0.09 ↔ 0.1 ↔ 0.11 et al, consider using the .padEnd() method on strings to keep the decimal places, like:

    const padTargetLength = this.value.startsWith("-") ? 5 : 4;
    this.previousElementSibling.previousElementSibling.textContent =
      this.value.padEnd(padTargetLength, "0");
    
    const rangeSliders = document.querySelectorAll("input");
    //const labels = document.querySelectorAll('label');
    const output1 = document.querySelector("#value-caps");
    const output2 = document.querySelector("#value-scaps");
    
    function updateProp(event) {
      const uom = this.dataset.uom || "";
      document.documentElement.style.setProperty(
        `--_${this.name}`,
        this.value + uom,
      );
    
      const padTargetLength = this.value.startsWith("-") ? 5 : 4;
      this.previousElementSibling.previousElementSibling.textContent =
        this.value.padEnd(padTargetLength, "0");
    }
    
    rangeSliders.forEach((input) => {
      input.addEventListener("input", updateProp);
    });
    * {
      outline: cornflowerblue dotted 0.5px;
    }
    
    * {
      margin: 0;
    }
    
     :root {
      --_trax-fontFamily-caps: 'Playfair Display', serif;
      --_trax-fontFamily-scaps: 'Playfair Display SC', serif;
      --_trax-fontFamily-body: 'Chivo', sans-serif;
      --_trax-fontFamily-mono: 'Chivo Mono', monospace;
      --_trax-textTracking-caps: var(--trax-textTracking-caps, 0.00cap);
      --_trax-textTracking-scaps: var(--trax-textTracking-scaps, 0.00ex);
      --_trax-textTracking-body: var(--trax-textTracking-body, 0.00em);
      --_trax-textTracking-mono: var(--trax-textTracking-mono, 0.00ch);
      --_trax-text-measure: var(--trax-text-measure, 66em);
    }
    
    body {
      box-sizing: content-box;
      margin-inline: auto;
      text-align: center;
      max-inline-size: var(--_trax-text-measure);
      padding-inline-start: 1rem;
      padding-inline-end: 1rem;
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: var(--_trax-fontFamily-mono);
    }
    
    h1 {
      font-size: 5rem;
    }
    
    h1.caps {
      font-family: var(--_trax-fontFamily-caps);
      letter-spacing: var(--_trax-textTracking-caps);
      text-transform: uppercase;
    }
    
    h1.scaps {
      font-family: var(--_trax-fontFamily-scaps);
      letter-spacing: var(--_trax-textTracking-scaps);
    }
    
    .align-self:flex-start,
    .asfs {
      align-self: flex-start;
    }
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Trax…text tracking tool</title>
      <link rel="preconnect" href="https://fonts.googleapis.com">
      <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
      <link href="https://fonts.googleapis.com/css2?family=Chivo&family=Chivo+Mono&family=Playfair+Display+SC:wght@700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
      <meta name="description" content="description">
    </head>
    
    <body>
    
      <p class="align-self:flex-start">Capitals</p>
      <h1 class="caps">Hamburgevons</h1>
      <div class="controls">
        <label for="trax-textTracking-caps" id="value-caps">0.00</label><span>&#8198;cap</span>
        <input name="trax-textTracking-caps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="cap">
        <button>CSS</button>
      </div>
    
      <p class="align-self:flex-start">Small Capitals</p>
      <h1 class="scaps">Hamburgevons</h1>
      <div class="controls">
        <label for="trax-textTracking-scaps" id="value-scaps">0.00</label><span>&#8198;ex</span>
        <input name="trax-textTracking-scaps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="ex">
        <button>CSS</button>
      </div>
    </body>
    </html>
    Login or Signup to reply.
  2. I don’t think the repetition in this case is inelegant. It would likely be best to leave it as is for readability instead of going the route of trying to put together something clever that will essentially do the same thing.

    As for the 2 decimals, I believe this would be the easiest way if you don’t mind the textContent being a number:

    output1.textContent = parseFloat(this.value).toFixed(2);
    output2.textContent = parseFloat(this.value).toFixed(2);
    

    Edit:

    In response to the comment below, this may be an alternate route to take since more sliders/labels will be added:

            const rangeSliders = document.querySelectorAll('input[type="range"]');
            const labels = document.querySelectorAll('label');
    
            function updateProp(event) {
                const uom = this.dataset.uom || '';
                document.documentElement.style.setProperty(`--_${this.name}`, this.value + uom);
    
                const label = labels[Array.from(rangeSliders).indexOf(this)];
                label.textContent = parseFloat(this.value).toFixed(2);
            }
    
            rangeSliders.forEach((input, index) => {
                input.addEventListener('input', updateProp);
                labels[index].textContent = parseFloat(input.value).toFixed(2);
            });
    

    This will only update the appropriate label based on the index of the current slider, and will fix the float length for you.

    Login or Signup to reply.
  3. For the elegancy part all you can do is put it in one line like output1 = output2 = this.value which works. For the decimal part I used a few if statements to convert it to decimals.

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Trax…text tracking tool</title>
        <link rel="preconnect" href="https://fonts.googleapis.com">
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
        <link href="https://fonts.googleapis.com/css2?family=Chivo&family=Chivo+Mono&family=Playfair+Display+SC:wght@700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
        <meta name="description" content="description">
        <style>
            * {
                outline: cornflowerblue dotted 0.5px;
            }
    
            * {
                margin: 0;
            }
    
            :root {
                --_trax-fontFamily-caps: 'Playfair Display', serif;
                --_trax-fontFamily-scaps: 'Playfair Display SC', serif;
                --_trax-fontFamily-body: 'Chivo', sans-serif;
                --_trax-fontFamily-mono: 'Chivo Mono', monospace;
    
                --_trax-textTracking-caps: var(--trax-textTracking-caps, 0.00cap);
                --_trax-textTracking-scaps: var(--trax-textTracking-scaps, 0.00ex);
                --_trax-textTracking-body: var(--trax-textTracking-body, 0.00em);
                --_trax-textTracking-mono: var(--trax-textTracking-mono, 0.00ch);
    
                --_trax-text-measure: var(--trax-text-measure, 66em);
            }
    
            body {
                box-sizing: content-box;
                margin-inline: auto;
                text-align: center;
                max-inline-size: var(--_trax-text-measure);
                padding-inline-start: 1rem;
                padding-inline-end: 1rem;
                display: flex;
                flex-direction: column;
                align-items: center;
                font-family: var(--_trax-fontFamily-mono);
            }
    
            h1 {
                font-size: 5rem;
            }
    
            h1.caps {
                font-family: var(--_trax-fontFamily-caps);
                letter-spacing: var(--_trax-textTracking-caps);
                text-transform: uppercase;
            }
    
            h1.scaps {
                font-family: var(--_trax-fontFamily-scaps);
                letter-spacing: var(--_trax-textTracking-scaps);
            }
    
            .align-self:flex-start,
            .asfs {
                align-self: flex-start;
            }
    
            /* add range slider code */
        </style>
    </head>
    
    <body>
    
        <p class="align-self:flex-start">Capitals</p>
        <h1 class="caps">Hamburgevons</h1>
        <div class="controls">
            <label for="trax-textTracking-caps" id="value-caps">0.00</label><span>&#8198;cap</span>
            <input name="trax-textTracking-caps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="cap">
            <button>CSS</button>
        </div>
    
        <p class="align-self:flex-start">Small Capitals</p>
        <h1 class="scaps">Hamburgevons</h1>
        <div class="controls">
            <label for="trax-textTracking-scaps" id="value-scaps">0.00</label><span>&#8198;ex</span>
            <input name="trax-textTracking-scaps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="ex">
            <button>CSS</button>
        </div>
    
        <script>
            const rangeSliders = document.querySelectorAll('input');
            //const labels = document.querySelectorAll('label');
            const output1 = document.querySelector('#value-caps');
            const output2 = document.querySelector('#value-scaps');
    
            function updateProp(event) {
                const uom = this.dataset.uom || '';
                document.documentElement.style.setProperty(`--_${this.name}`, this.value + uom);
                let value = this.value;
                if (Math.abs(value).toString().length < 4){
                    value += "0";
                    if (value == "00"){
                      value = "0.00";
                    }
                }
                
                output1.textContent = output2.textContent = value;
            }
    
            rangeSliders.forEach(input => {
                input.addEventListener('input', updateProp);
            });
        </script>
    
    </body>
    
    </html>
    Login or Signup to reply.
  4. One approach is as follows, with explanatory comments in the code:

    // utilities, largely to save typing and, in the case of getAll(), to return an Array
    // of element nodes in order to allow use of Array methods (filter(), map(), etc...):
    const D = document,
      get = (selector, context = D) => context.querySelector(selector),
      getAll = (selector, context = D) => [...context.querySelectorAll(selector)],
      roundTo = (n, d = 0) => Math.round(parseFloat(n) * Math.pow(10, d)) / Math.pow(10, d);
    
    // retrieving an Array of all the <input> elements of type="range":
    const sliders = getAll('input[type=range]');
    
    // defining the function to handle the updates, this takes
    // one argument: a reference to the Event Object automatically
    // passed from the (later) use of EventTarget.addEventListener():
    const update = (evt) => {
      // using destructuring assignment, to retrieve the named properties
      // of the Event Object, the currentTarget property is renanamed to
      // "changed" (this is my own preference, this can be changed or
      // ommitted) and is a reference to the Element to which the
      // function was bound (in this case the <input type="range"> elements),
      // the isTrusted property is a Boolean property that specifies
      // whether the Event was initiated by a user (true) or programatically
      // (false):
      let {
        currentTarget: changed,
        isTrusted,
      } = evt,
      // again using destructuring assignment, retrieving the named properties
      // of the changed Element and assinging those property-values to variables
      // with the same name as the property from which they came:  
      {
        labels,
        value
      } = changed,
      // retrieving the closest <div> ancestor of the current <input> element:
      wrapper = changed.closest('div'),
        // using parseFloat() to recover the numberical value of the entered
        // number:
        valueAsNumber = parseFloat(value),
        // as per your own code, retrieving the data-uom attribute-value:
        uom = changed.dataset.uom,
        // establishing whether the entered value is negative:
        isNegative = valueAsNumber < 0;
    
      // if the function was called programatically, then we run these
      // initialisation tasks:
      if (false === isTrusted) {
        // we iterate over all the <label> elements associated with the
        // <input> (this association can be implicit, with the <input>
        // nested within the <label>; or explicit, with the "for"
        // attribute-value being exactly equal to the "id" attribute-
        // value of the <input> (this is why I added the "id"
        // attribute to the <input> in the HTML, and one of many
        // reasons this association should be specified):
        labels.forEach(
          // iterating over the <label> element collection:
          (lbl) => {
            // we retrieve the next element-sibling:
            let sibling = lbl.nextElementSibling;
            // if that element matches the supplied CSS selector:
            if (sibling.matches('span')) {
              // we set the text content to the following, which
              // is exactly the content you set yourself but -
              // because I don't like having to do unnecessary work
              // - I chose to use JavaScript to set it initially
              // since we're pretty much iterating over everything
              // anyway:
              sibling.textContent = `u2006${uom}`;
            }
          });
      }
    
      // here we iterate over the <label> elements in every case, whether
      // the function was user-initiated or not:
      labels.forEach(
        (lbl) => {
          // updating the classList of the <label>, adding the class
          // numberIsNegative if the entered value is negative (number < 0);
          // if the number is zero or larger then the class is removed. This
          // generates no errors if the class was, or was not, already present
          // and this does not lead to multiple instances of the same
          // class-name being added (or removed). I'm taking this
          // approach because I dislike layouts jumping around when a number
          // changes to, or from, a negative number so I'm using CSS
          // to indicate to visual users that a number is negative:
          lbl.classList.toggle('numberIsNegative', isNegative);
    
          // because I'm using CSS to display the negative state of an
          // entered number, I'm using Math.abs() to return the absolute
          // value of that number, and then - because I hate layout jumps -
          // I'm passing that absolute value to the utility function
          // roundTo(), the first argument is the number to be rounded, and
          // the second is the number of decimals to which the number
          // should be rounded. This rounded number is then passed to
          // Number.prototype.toFixed(), which takes the number and then
          // either truncates a longer number down to a specified number
          // of decimal places, or extends that number to the specified
          // number of decimal places; this method returns a String:
          lbl.textContent = roundTo(Math.abs(valueAsNumber), 2).toFixed(2);
          // given that the styles are set on the :root elements, here
          // I use the get() function to retrieve that :root element
          // (there's no obvious benefit to this approach over your
          // own, but it is less typing):
          get(':root').style.setProperty(`--${changed.name}`, valueAsNumber + uom)
        });
    }
    
    // creating a custom Event(), to be used in order to trigger the event
    // that we're listening for on the <input> elements:
    const fakeInputEvent = new Event('input');
    
    // iterating over the Array of <input type=range> elements:
    sliders.forEach(
      (el) => {
        // using EventTarget.addEventListener() to bind the update()
        // function as the event-handler of the 'input' event
        // triggered on the current element:
        el.addEventListener('input', update);
        // calling EventTarget.dispatchEvent() to initialise the
        // display on page-load:
        el.dispatchEvent(new Event('input'));
      });
    * {
      /* combining the properties you set, and the usual resets that I
         prefer to use in order to reduce cross-browser differences: */
      box-sizing: border-box;
      margin: 0;
      outline: cornflowerblue dotted 0.5px;
      padding: 0;
    }
    
    :root {
      --_trax-fontFamily-caps: 'Playfair Display', serif;
      --_trax-fontFamily-scaps: 'Playfair Display SC', serif;
      --_trax-fontFamily-body: 'Chivo', sans-serif;
      --_trax-fontFamily-mono: 'Chivo Mono', monospace;
      --_trax-textTracking-caps: var(--trax-textTracking-caps, 0.00cap);
      --_trax-textTracking-scaps: var(--trax-textTracking-scaps, 0.00ex);
      --_trax-textTracking-body: var(--trax-textTracking-body, 0.00em);
      --_trax-textTracking-mono: var(--trax-textTracking-mono, 0.00ch);
      --_trax-text-measure: var(--trax-text-measure, 66em);
    }
    
    body {
      box-sizing: content-box;
      margin-inline: auto;
      text-align: center;
      max-inline-size: var(--_trax-text-measure);
      padding-inline-start: 1rem;
      padding-inline-end: 1rem;
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: var(--_trax-fontFamily-mono);
    }
    
    h2 {
      /* here we define the properties that should be used for the
         font-family and letter-spacing, along with acceptable
         defaults:  */
      font-family: var(--chosenVariant, system-ui);
      font-size: 5rem;
      letter-spacing: var(--letterSpacing, normal);
    }
    
    .caps {
      /* here we simply set the custom properties that each
         matching element should use; this can lead to
         less code repetition: */
      --chosenVariant: var(--_trax-fontFamily-caps);
      --letterSpacing: var(--_trax-textTracking-caps);
      text-transform: uppercase;
    }
    
    .scaps {
      --chosenVariant: var(--_trax-fontFamily-scaps);
      --letterSpacing: var(--_trax-textTracking-scaps);
    }
    
    .align-self:flex-start,
    .asfs {
      align-self: flex-start;
    }
    
    
    /* to avoid layout shifts, we use position: relative
       on the <label> elements: */
    
    label {
      position: relative;
    }
    
    
    /* and set a transition on the opacity of the
       ::before pseudo-element: */
    
    label::before {
      content: '';
      opacity: 0;
      transition: opacity 1s linear;
    }
    
    
    /* when the <label> has the .numberIsNegative class-name,
       added via the JavaScript, the pseudo-element: */
    
    label.numberIsNegative::before {
      /* using a custom property to set the size of the
         pseudo element: */
      --signSize: 1.2rem;
      /* shows the symbol for the negative mathematical operand,
         which is not the same as the hyphen (2010), the
         hyphen-minus (02d), the non-breaking hyphen (2011),
         the figure dash (2012), en-dash (2013), em-dash (2014)
          or so very mmany others */
      content: "2212";
      inline-size: var(--signSize);
      /* using inset to place the pseudo element, this is an
         easier way of specifying:
           top: 0;
           right: auto;
           bottom: 0;
           left: calc(var(--signSize) * -1);
           
         this allows us to move the element out of
         the parent, which means it won't affect
         the layout when it appears or disappears:
       */
      inset: 0 auto 0 calc(var(--signSize) * -1);
      opacity: 1;
      /* in order to take the element out of the
         document flow in order to negate its
         impact on layout: */
      position: absolute;
    }
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Chivo&family=Chivo+Mono&family=Playfair+Display+SC:wght@700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet">
    
    <p>Capitals</p>
    <!-- a <h1> element has a specific meaning, and use: it's there to provide a title for the
         whole document; there should be only one per page. An <h2> element, is used to title a
         smaller section of the document and can occur multiple times. Therefore I've replaced
         your <h1> elements with <h2> elements: -->
    <h2 class="caps">Hamburgevons</h2>
    <div class="controls">
      <label for="trax-textTracking-caps" id="value-caps">0.00</label>
      <!-- removed the content from the <span> elements, because why work
           when you can set the program up to do it for you (it's also more
           accurate and more reliable that way, so long as it's coded and
           tested correctly) -->
      <span></span>
      <!-- all <input> elements should be associated with a relevant <label>, to do so
           that <input> should be either nested within the <label> (implicit association)
           or the <label> should have a "for" attribute, the value of which should be
           exactly equal to the "id" attribute-value of the <input> (explicit
           association); as this <input> is not nested it must have an "id" attribute,
           which is why I specified one, and simply copied the "name" attribute: -->
      <input id="trax-textTracking-caps" name="trax-textTracking-caps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="cap">
      <button>CSS</button>
    </div>
    
    <p>Small Capitals</p>
    <h2 class="scaps">Hamburgevons</h2>
    <div class="controls">
      <label for="trax-textTracking-scaps" id="value-scaps">0.00</label>
      <span></span>
      <input id="trax-textTracking-scaps" name="trax-textTracking-scaps" type="range" min="-0.30" max="0.30" value="0.00" step="0.01" data-uom="ex">
      <button>CSS</button>
    </div>

    JS Fiddle demo.

    References:

    Bibliography:

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search