最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to create a scientific HTML slider with input type="range" - Stack Overflow

programmeradmin1浏览0评论

I need to create a scientific input slider. More precisely:

  • n ticks, example: no matter the range (0 - 200, 1 - 11, etc.), I want 11 ticks covering the whole range
  • the number value label below each tick
  • no "glue" attraction effect when the slider cursor is near a tick

Mockup:

Note:

  • this is not a duplicate of Ticks for type="range" HTML input because of these 3 points.

  • browser support: at least Chrome

The following code produces a HTML slider with ticks, and it works. However, it fails both the 2nd and 3rd criteria above.

input { width: 400px; }
<input type=range min=0 max=200 value=0 step=1 list=tickmarks>
<datalist id=tickmarks>
    <option>0</option>
    <option>20</option>
    <option>40</option>
    <option>60</option>
    <option>80</option>
    <option>100</option>
    <option>120</option>
    <option>140</option>
    <option>160</option>
    <option>180</option>
    <option>200</option>
 </datalist>

I need to create a scientific input slider. More precisely:

  • n ticks, example: no matter the range (0 - 200, 1 - 11, etc.), I want 11 ticks covering the whole range
  • the number value label below each tick
  • no "glue" attraction effect when the slider cursor is near a tick

Mockup:

Note:

  • this is not a duplicate of Ticks for type="range" HTML input because of these 3 points.

  • browser support: at least Chrome

The following code produces a HTML slider with ticks, and it works. However, it fails both the 2nd and 3rd criteria above.

input { width: 400px; }
<input type=range min=0 max=200 value=0 step=1 list=tickmarks>
<datalist id=tickmarks>
    <option>0</option>
    <option>20</option>
    <option>40</option>
    <option>60</option>
    <option>80</option>
    <option>100</option>
    <option>120</option>
    <option>140</option>
    <option>160</option>
    <option>180</option>
    <option>200</option>
 </datalist>

Is there an attribute for an HTML <input type="range"> to enable these "number-labelled" ticks?

Is an implementation that satisfies all three criteria possible?

Share Improve this question edited Mar 24, 2023 at 14:13 Basj asked Mar 21, 2023 at 14:15 BasjBasj 46.5k109 gold badges452 silver badges798 bronze badges 9
  • This feels like a web component waiting to happen, but I don't think browsers do this natively. – somethinghere Commented Mar 21, 2023 at 14:28
  • According to the WHATWG spec, browsers should respect\ <option label="0"> for the tickmarks, but it doesn't look like any of them do. – Dai Commented Mar 21, 2023 at 14:38
  • this is not supported by default, i have a solution but i used some css and extra html markup to achieve it, since the question has been closed i can't add my answer – mmh4all Commented Mar 21, 2023 at 14:41
  • @mmh4all Great, can you share in a JSfiddle ? I'll vote to reopen the question (you can too). – Basj Commented Mar 21, 2023 at 14:44
  • 1 December last year I created this Codepen based on another SO question, maybe it will give you some alternative to toy with. In the remainder of the CSS you will find my solution for your issue (search for SO75802357). Not posted as an answer as the glue effect is not solved... – Rene van der Lende Commented Mar 24, 2023 at 16:05
 |  Show 4 more comments

6 Answers 6

Reset to default 5 +100

As Lajos Arpad's answer indicates, it's best to avoid using datalist as your requirements go beyond what it's capable of. You'll need to manually create the ticks with number values.

The following code uses a HTML layout similar to the original code in your question, with styling to suit. Note that the width is set on a parent element, and if the parent is too small then the tick numbers will wrap to a new line. To counter this you may need to decrease the tick number font size.

To get the ticks lining up properly across all major browsers it is perhaps better to style the slider so it looks identical across browsers. Styles targeting Chrome- and Firefox-specific pseudo-elements for range sliders are included. The result should look the same across all Chromium and Mozilla based browsers.

Note that a truly cross-browser method of filling in the range slider, as shown in the question, can only be achieved with JavaScript. The JavaScript here is basically copied from this answer

const input = document.querySelector("#input input")
input.addEventListener( 'input', function() {
    const value = (this.value-this.min)/(this.max-this.min)*100;
    this.style.background = 'linear-gradient(to right, #35b0f2 0%, #35b0f2 ' + value + '%, #ccc ' + value + '%, #ccc 100%)';
} );
#input {
    width: 400px;
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}
#input span {
    position: relative;
    margin: 15px -5px 0 -5px;
    flex-basis: 0;
    flex-grow: 1;
    text-align: center;
    font-size: 0.85em;
    user-select: none;
}
#input span::after {
    content: "";
    position: absolute;
    width: 1px;
    height: 8px;
    left: 0;
    right: 0;
    margin: auto;
    top: -12px;
    background-color: #ccc;
}
#input input {
    width: 100%;
    margin: 5px 10px;
    position: relative;
    background-color: #ccc;
    border-radius: 99px;
    z-index: 10;
    height: 7px;
    -webkit-appearance: none;
}
#input input::-moz-range-thumb {
    border: none;
    height: 16px;
    width: 16px;
    border-radius: 99px;
    background: #35b0f2;
    cursor: pointer;
}
#input input::-webkit-slider-thumb {
  box-shadow: none
  border: none;
  height: 16px;
  width: 16px;
  border-radius: 99px;
  background-color: #35b0f2;
  cursor: pointer;
  -webkit-appearance: none;
}
<div id="input">
    <input type=range min=0 max=200 value=0 step=1>
    <span>0</span>
    <span>20</span>
    <span>40</span>
    <span>60</span>
    <span>80</span>
    <span>100</span>
    <span>120</span>
    <span>140</span>
    <span>160</span>
    <span>180</span>
    <span>200</span>
</div>

You can use a datalist with a list of option, but it will require you to add label texts as the value will not be used as tickmark values.

generic setup (MDN: input type="range", Adding tick marks)

    <input type="range" list="tickmark-list">
    <datalist id="tickmark-list">
        <option value=1 label="1"/>
        <option value=N label="N"/>
    </datalist>

For the labels you have two options:

option 1: use label with text

<option value="1" label="tickmark-value-1"/>
<option value="N" label="tickmark-value-N"/>

option 2: use an option text

<option value="1">tickmark-value-1</option>
<option value="N">tickmark-value-N</option>

When using both a label text and option text, the latter will be ignored. For the snippet I used option 1.

tickmarks

  • input type="range" and datalist must have equal width. In the snippet I used CSS custom property --slider-width: 100% to set the width of either element.
  • datalist as Flexbox row container with justify-content: space-between is perfectly suited to distribute the tickmark labels. It pushes the outer labels to their respective parent left and right margins, while evenly distributing the rest of the labels over the remaining space.
  • For even distribution of tickmark texts it is best to give them all a width equal to the largest text. In the snippet I took the width of label 200.
  • To center the label text below the tickmarks use text-align: center.
  • The tickmarks are default inserted by the range slider itself and does not require any extra code.

Depending on label texts used some fiddling with margin/padding of option:first-child and/or option:last-child might be required.

Bonus in the snippet I added class .vertical to show how you can use flex-direction: column and writing-mode: vertical-lr to easily get 90 degree rotated tickmark labels.

UPDATE

As initially commented the above method does not solve the 'glue' effect caused by the datalist tickmarks in Firefox browsers. Additionally, Firefox opens an option select list upon second click at a tickmark, treating a datalist as a select. While Firefox specific behavior and probably known/preferred by users, it may be unwanted for a site.

However, after investigation, it appears the forementioned behavior is very hard, if not impossible, to disable. One could opt to create a fully custom range slider with Javascript, but that may prove to be overkill: create a workaround for a single browser, disabling behavior that may or may not be changed in the future.

Tried methods that fail:

  • datalist,option { pointer-events: none }
  • <option disabled /> works, but removes tickmark
  • <datalist type="range" autocomplete="off">
  • <input type="range" autocomplete="off">
  • <option autocomplete="off">
  • <option label=".."/> without value=".."
  • <option value="..">..</option> text instead of label
  • embed <input type="range"> in a <form> with all elements autocomplete="off"
  • Disable autocomplete history in browser Privacy Settings

:root  { --slider-width: 100% }

input, datalist  {
    width: var(--slider-width);
}

datalist {
    display: flex;
    flex-direction: row;
    justify-content: space-between;
}

option {
    text-align: center;
    width: 1.78rem; /* ~width of label '200' */
}

.vertical {
    flex-direction: column;
    writing-mode: vertical-lr;
}
input[list="tickmarks2"] { margin: 0 }

.vertical option { text-align: right; width: 1.5rem }
<div id="SO75802357">
    <input type=range min=0 max=200 value=0 step=1 list="tickmarks1">
    <datalist id="tickmarks1">
        <option value=0   label="0"  />
        <option value=20  label="20" />
        <option value=40  label="40" />
        <option value=60  label="60" />
        <option value=80  label="80" />
        <option value=100 label="100"/>
        <option value=120 label="120"/>
        <option value=140 label="140"/>
        <option value=160 label="160"/>
        <option value=180 label="180"/>
        <option value=200 label="200"/>
    </datalist>
    <br><br>
    <input type=range min=0 max=200 value=0 step=1 list="tickmarks2">
    <datalist id="tickmarks2" class="vertical">
        <option value=0   label="0"  />
        <option value=20  label="20" />
        <option value=40  label="40" />
        <option value=60  label="60" />
        <option value=80  label="80" />
        <option value=100 label="100"/>
        <option value=120 label="120"/>
        <option value=140 label="140"/>
        <option value=160 label="160"/>
        <option value=180 label="180"/>
        <option value=200 label="200"/>
    </datalist>
</div>

One possible solution is to dynamically create a scale. There are many details to consider, such as steps. You can start here:

document.querySelectorAll('.range').forEach(el => {
  const input = el.querySelector('input');
  if(!input){
    return;
  }
  const min = Number(input.min) || 0;
  const step = Number(input.step) || 1;
  const max = Number(input.max);
  let ticks = Number(input.dataset.ticks);
  if(!max || !ticks || step !== 1 ){
    return;
  }
  if(ticks > max - min ){
    ticks = max - min + 1;
  }
  ticks--;
  let increment = (max - min) / ticks;
  let value = min;
  let html = '';
  for(let i = 0; i <= ticks; i++ ){
    html += `<div class="range-tick">${value.toFixed()}</div>`
    value += increment;
  }
  el.insertAdjacentHTML('beforeend', `<div class="range-ticks">${html}</div>`);
})
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
.range {
  width: 100%;
  max-width: 600px;
  --thumb-size: 12px;
  --track-size: 2px;
  --track-color: #ccc;
  margin-top: calc(var(--thumb-size) / 2);
}
.range input {
  display: block;
  width: 100%;
  -webkit-appearance: none;
  background: none;
}
.range input::-webkit-slider-thumb {
  width: var(--thumb-size);
  height: var(--thumb-size);
  -webkit-appearance: none;
  border-radius: 50%;
  background-color: blue;
  margin-top: calc(var(--thumb-size) / -2 + var(--track-size) / 2);
}
.range input::-webkit-slider-runnable-track {
  height: var(--track-size);
  background-color: var(--track-color);
  -webkit-appearance: none;
}
.range-ticks {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
}
.range-tick {
  font-size: 14px;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: var(--thumb-size);
}
.range-tick:before {
  content: '';
  width: 2px;
  height: 10px;
  margin-bottom: 2px;
  background-color: var(--track-color);
}
<div class="range">
  <input type="range" min="0" max="200" value="0" step="1" data-ticks="11" oninput="output.value = this.value">
</div>
<output id="output">0</output>

You can achieve this by putting a div below your range that you divide into sections. The code below is just a proof-of-concept, which already works, but when you actually integrate it into your project, you will need to make it reusable and adjustable.

window.addEventListener("load", function() {
    let div = document.getElementById("ranger-div");
    let html = [];
    for (let i = 0; i <= 200; i += 20) {
        html.push(`<div><div style="with:100%;">|</div>${i}</div>`);
    }
    div.innerHTML = html.join("");
});
.ranger { width: 400px; }
#ranger-div {display: inline-flex; width: 440px;}
#ranger-div > div {
    color: red;
    width: 100%;
}
<input class="ranger" type=range min=0 max=200 value=0 step=1>
<div id="ranger-div"></div>

class ScientificInput extends HTMLDivElement {
  constructor() {
    super();
    this.step = "any";
    this.ticks = 11;
    this.canvasHeight = 24;
  }

  createSvgElement(elementName) {
    let element = document.createElementNS(
      "http://www.w3.org/2000/svg",
      elementName
    );
    return element;
  }

  createTickLines(x1, y1, x2, y2) {
    let tickColor = this.getAttribute("tColor");
    let newLine = this.createSvgElement("line");
    newLine.setAttribute("x1", x1);
    newLine.setAttribute("y1", y1);
    newLine.setAttribute("x2", x2);
    newLine.setAttribute("y2", y2);
    if (tickColor) {
      newLine.setAttribute("stroke", tickColor);
    } else {
      newLine.setAttribute("stroke", "black");
    }
    return newLine;
  }

  createTickNumbers(txt, x, y) {
    let numberColor = this.getAttribute("nColor");
    let newText = this.createSvgElement("text");
    newText.setAttribute("font-size", "12px");
    if (txt < 1 && txt !== 0) {
      txt = parseFloat(txt.toFixed(4));
      txt = txt.toExponential();
    }
    if (txt > 1000) {
      txt = txt.toExponential();
    }

    if (numberColor) {
      newText.setAttribute("fill", numberColor);
    }

    newText.textContent = txt;
    newText.setAttributeNS(null, "x", x);
    newText.setAttributeNS(null, "y", y);
    return newText;
  }

  drawTicks(svg) {
    this.textNodes = [];
    const step = (this.max - this.min) / 10;
    let currentWidth = parseFloat(
      getComputedStyle(this).width.replace("px", "")
    );
    const factor = (currentWidth - 16) / (this.max - this.min);
    for (let tick = 0; tick < 11; tick++) {
      const horizontalOffset = Math.ceil(tick * step * factor + 8);
      const line = this.createTickLines(
        horizontalOffset,
        0,
        horizontalOffset,
        5
      );
      svg.append(line);
      const text = this.createTickNumbers(
        tick * step + this.min,
        horizontalOffset,
        20
      );
      svg.append(text);
      this.textNodes.push(text);
    }
  }

  adjustTextPosition() {
    this.textNodes.forEach((e, i) => {
      const textWidth = Math.round(e.getBBox().width);
      if (i === 0) {
        e.setAttribute("transform", `translate(-${5} 0)`);
      } else if (i === 10) {
        e.setAttribute("transform", `translate(-${textWidth - 5} 0)`);
      } else {
        e.setAttribute("transform", `translate(-${textWidth / 2} 0)`);
      }
    });
  }

  createContainer() {
    const container = document.createElement("div");
    container.setAttribute("id", "container");
    container.style.width = "100%";
    container.style.display = "flex";
    container.style.flexFlow = "column";
    return container;
  }

  createSvg() {
    let svg = this.createSvgElement("svg");
    svg.setAttributeNS(
      "http://www.w3.org/2000/xmlns/",
      "xmlns:xlink",
      "http://www.w3.org/1999/xlink"
    );
    svg.setAttribute("width", "100%");
    svg.setAttribute("height", this.canvasHeight);
    return svg;
  }

  createRange() {
    const range = document.createElement("input");
    range.type = "range";
    range.min = this.min;
    range.max = this.max;
    range.step = this.step;
    range.style.margin = "0";
    range.style.width = "100%";
    range.addEventListener("change", (e) => console.log(e.target.value));
    return range;
  }

  render() {
    this.width = parseFloat(this.getAttribute("width"));
    this.min = parseFloat(this.getAttribute("min"));
    this.max = parseFloat(this.getAttribute("max"));

    this.style.width = this.width + "px";

    // create a container
    this.container = this.createContainer();

    // create and set input control
    const range = this.createRange();

    // create a svg
    this.svg = this.createSvg();

    // draw the ticks on the canvas
    this.drawTicks(this.svg);

    // append the input to container
    this.container.append(range);

    // append the svg to container
    this.container.append(this.svg);

    // append the container to this instance
    this.append(this.container);

    // center the numbers
    this.adjustTextPosition();

    new ResizeObserver(() => {
      this.svg.remove();
      this.svg = this.createSvg();
      this.drawTicks(this.svg);
      this.container.append(this.svg);
      this.adjustTextPosition();
    }).observe(this.container);
  }

  connectedCallback() {
    this.render();
  }
}

customElements.define("scientific-input", ScientificInput, {
  extends: "div"
});
body {
  font-family: sans-serif;
}

.s-input {
  margin-bottom: 2em;
}

input[type="range"]::-moz-range-thumb {
  box-sizing: border-box;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background-color: #0375ff;
}

input[type="range"]::-moz-range-progress {
  background-color: #0375ff;
}

input[type="range"]::-moz-range-track {
  background-color: gray;
  border-radius: 5px;
}
<div class="s-input" is="scientific-input" min="0" max="10" width="300" tColor="forestGreen" nColor="hotPink"></div>
<div class="s-input" is="scientific-input" min="0" max="100" width="400" tColor="darkOrange" nColor="forestGreen"></div>
<div class="s-input" is="scientific-input" min="100" max="700" width="500"></div>
<div class="s-input" is="scientific-input" min="0" max="10000" width="500"></div>
<div class="s-input" is="scientific-input" min="0" max="0.007" width="500" ></div>

I have created another solution for this one with web component, with it you have far more control and customization.

According to me and as everyone suggested in this answer It is recommended to not use datalist if your needs exceed its capabilities. Instead, you should manually create the ticks with numerical values.

const input = document.querySelector("#rangeSelector input");
const updateRangeSelector = () => {
  const value = (input.value - input.min) / (input.max - input.min) * 100;
  input.style.background = `linear-gradient(to right, #0500ff 0%, #3549f2 ${value}%, #ccc ${value}%, #ccc 100%)`;
};
input.addEventListener('input', updateRangeSelector);
#rangeSelector {
  width: 400px;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
}

#rangeSelector input {
  width: 100%;
  margin: 5px 10px;
  position: relative;
  background-color: #ccc;
  border-radius: 99px;
  z-index: 10;
  height: 7px;
  -webkit-appearance: none;
}

#rangeSelector input::-moz-range-thumb {
  border: none;
  height: 16px;
  width: 16px;
  border-radius: 99px;
  background: #0500ff;
  cursor: pointer;
}

#rangeSelector input::-webkit-slider-thumb {
  box-shadow: none border: none;
  height: 16px;
  width: 16px;
  border-radius: 99px;
  background-color: #0500ff;
  cursor: pointer;
  -webkit-appearance: none;
}

#rangeSelector span {
  position: relative;
  margin: 15px -5px 0 -5px;
  flex-basis: 0;
  flex-grow: 1;
  text-align: center;
  font-size: .85em;
  user-select: none;
}

#rangeSelector span::after {
  content: "";
  position: absolute;
  width: 1px;
  height: 8px;
  left: 0;
  right: 0;
  margin: auto;
  top: -12px;
  background-color: #ccc;
}
<div id="rangeSelector">
  <input type=range min=0 max=200 value=0 step=1 list=tickmarks>
  <span>0</span>
  <span>20</span>
  <span>40</span>
  <span>60</span>
  <span>80</span>
  <span>100</span>
  <span>120</span>
  <span>140</span>
  <span>160</span>
  <span>180</span>
  <span>200</span>
</div>

发布评论

评论列表(0)

  1. 暂无评论