Making a input range that perfectly aligns with numbers above it

Viewed 60

I have a slider that goes from 1 to 12. I would like the thumb to perfectly align with all the numbers above it when I move it up and down.

This is what I have so far, but as you can see, it is still slightly off.

Is there a way to achieve this?

body {
  height: 100vh;
  display: flex;
}

.container {
  margin: auto;
  width: 400px;
}

span {
  display: block;
  width: 1ch;
}

.numbers {
  display: flex;
  justify-content: space-between;
  padding-left: .4em;
}
<div class="container">
  <div class="numbers">
    <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span><span>11</span><span>12</span>
  </div>
  <input style="width: 100%;" type="range" min="1" max="12" name="" id="" />
</div>

2 Answers

We can configure the number spans so they all have the same width and center text alignment. By setting their initial width to zero we negate the effect of their contents, which vary in size (1 vs 10, for example).

We can use flexbox on the slider as well, giving it the same flex-grow value as twice our 12 numbered spans. This lets us use half-widths for spacing. We add a span on each side of it with a value of half that of the numbered spans, shifting things over properly.

Finally, some margin tweaks get things pixel-perfect (in Chrome, at least).

Note that I've changed the primary width to a relative value so you can see that this works at any size. Have a look at the full screen demo.

The primary drawback here is that we have quantities hard-coded in the CSS. If the slider range changes, so must the CSS. Ideally we'd find a fully flexible solution.

If I'm not making sense, have a look at https://css-tricks.com/snippets/css/a-guide-to-flexbox.

body {
  height: 100vh;
  display: flex;
}

.container {
  margin: auto;
  width: 75vw;
}

.numbers,
.slider {
  display: flex;
  justify-content: space-between;
}

.numbers span,
.slider span {
  width: 0;
  flex-grow: 1;
  text-align: center;
}

.slider span {
  flex-grow: 1;
}

.slider input {
  flex-grow: 24;
  margin-left: -2px; /* account for UI sizing */
  margin-right: -2px; /* account for UI sizing */
}
<div class="container">
  <div class="numbers">
    <span>1</span><span>2</span><span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span><span>11</span><span>12</span>
  </div>

  <div class="slider">
    <span><!-- half --></span>
    <input type="range" min="1" max="12" name="" value="1" id="" />
    <span><!-- half --></span>
  </div>
</div>

I think they can't be easily arranged, according to the idea you have to play with a margin left right or width : 0%; etc ...

If you want it is an alternative

const range = document.getElementById("range"),
    rangeV = document.getElementById("rangeV"),
    setValue = () => {
        const newValue = Number(((range.value - range.min) * 100) / (range.max - range.min)),
            newPosition = 10 - newValue * 0.2;
        rangeV.innerHTML = `<span>${range.value}</span>`;
        rangeV.style.left = `calc(${newValue}% + (${newPosition}px))`;
    };
document.addEventListener("DOMContentLoaded", setValue);
range.addEventListener("input", setValue);
body {
    min-height: 100vh;
    padding: 0 10vh;
    margin: 0;
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}
input[type="range"] {
    -webkit-appearance: none;
    margin: 20px 0;
    width: 100%;
}
input[type="range"]:focus {
    outline: none;
}
input[type="range"]::-webkit-slider-runnable-track {
    width: 100%;
    height: 4px;
    cursor: pointer;
    animate: 0.2s;
    background: #03a9f4;
    border-radius: 25px;
}
input[type="range"]::-webkit-slider-thumb {
    height: 20px;
    width: 20px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 0 4px 0 rgba(0, 0, 0, 1);
    cursor: pointer;
    -webkit-appearance: none;
    margin-top: -8px;
}
input[type="range"]:focus::-webkit-slider-runnable-track {
    background: #03a9f4;
}
.range-wrap {
    width: 500px;
    position: relative;
}
.range-value {
    position: absolute;
    top: -50%;
}
.range-value span {
    width: 30px;
    height: 24px;
    line-height: 24px;
    text-align: center;
    background: #03a9f4;
    color: #fff;
    font-size: 12px;
    display: block;
    position: absolute;
    left: 50%;
    transform: translate(-50%, 0);
    border-radius: 6px;
}
.range-value span:before {
    content: "";
    position: absolute;
    width: 0;
    height: 0;
    border-top: 10px solid #03a9f4;
    border-left: 5px solid transparent;
    border-right: 5px solid transparent;
    top: 100%;
    left: 50%;
    margin-left: -5px;
    margin-top: -1px;
}
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>Input Range with Dynamic Label</title>
    </head>
    <body>
        <div class="range-wrap">
            <div class="range-value" id="rangeV"></div>
            <input id="range" type="range" min="1" max="12" value="1" step="1" />
        </div>
    </body>
</html>

Related