I am trying to create sliders with some ticks and labels. To add ticks and labels, I am using datalist. This is for a flask app. in this app, we will receive some variables and I want to create a range slider for each of these variables. here is an example of variables:
var1 = {'name': 'rooms', 'type':'cont', 'sample_value':50, 'min':20.5, 'max':80, 'Q1':30.3, 'med':50.9, 'Q3':70.4, 'step':0.1}
var2 = {'name': 'distance', 'type':'cont', 'sample_value':34.5, 'min':0, 'max':44.8, 'Q1':12.3, 'med':20.9, 'Q3':36.4, 'step':0.1}
var3 = {'name': 'price', 'type':'cont', 'sample_value':66, 'min':30, 'max':200, 'Q1':80.3, 'med':130, 'Q3':100.4, 'step':0.1}
variables = [var1, var2, var3]
There are two problems with current form of sliders:
here is HTML code:
{% for variable in variables %}
<div class="range-wrap">
<label class="slider-label" for="{{variable.name}}">{{ variable.name }}</label>
<output class="bubble"></output>
<input type="range" class="range" id="{{ variable.name }}" name="{{ variable.name }}"
value="{{ variable.sample_value }}" list="{{ variable.name }}" min="{{ variable.min }}"
max="{{ variable.max }}" step="{{ variable.step }}" >
<datalist id="{{ variable.name }}">
<option value="{{ variable.min }}" label="Min"></option>
<option value="{{ variable.Q1 }}" label="1st Q"></option>
<option value="{{ variable.med }}" label="Median"></option>
<option value="{{ variable.Q3 }}" label="3rd Q"></option>
<option value="{{ variable.max }}" label="Max"></option>
</datalist>
</div>
{% endfor %}
<input type="submit" value="Submit" name="action" class="btn btn-default">
<input type="submit" value="Reset" name="action" class="btn btn-default">
<script>
const allRanges = document.querySelectorAll(".range-wrap");
allRanges.forEach(wrap => {
const range = wrap.querySelector(".range");
const bubble = wrap.querySelector(".bubble");
range.addEventListener("input", () => {
setBubble(range, bubble);
});
setBubble(range, bubble);
});
function setBubble(range, bubble) {
const val = range.value;
const min = range.min ? range.min : 0;
const max = range.max ? range.max : 100;
const newVal = Number(((val - min) * 100) / (max - min));
bubble.innerHTML = val;
// Sorta magic numbers based on size of the native UI thumb
bubble.style.left = `calc(${newVal}% + (${8 - newVal * 0.15}px))`;
}
</script>
CSS:
<style>
datalist {
display: flex;
flex-direction: row;
justify-content: space-between;
writing-mode: horizontal-tb;
width: 100%;
}
option {
padding: 0;
}
.range-wrap {
position: relative;
margin: 0 auto 3rem;
}
.slider-label {
position: relative;
margin: 0 auto 3rem;
}
.range {
width: 500px;
}
.bubble {
background: rgb(12, 83, 163);
color: white;
padding: 4px 12px;
position: absolute;
border-radius: 4px;
left: 50%;
bottom: 48px;
transform: translateX(-50%);
}
.bubble::after {
content: "";
position: absolute;
width: 2px;
height: 2px;
background: rgb(176, 167, 218);
bottom: -2px;
left: 50%;
}
body {
margin: 2rem;
}
</style>
I expect to have labels at the right values and ticks be shown under the slider.
A unique id for the datalist is required to display the ticks. You accidentally used the same id for the datalist and the range-slider.
Unfortunately, the label in the option element of the datalist is not displayed by every browser. So I added other elements that compensate for this peculiarity. Their position is set in a similar way to how you set the bubble. The position is absolute and the left property is set by css variables. These variables are assigned within the template in inline style elements. The values correspond to the data passed to the template.
I'm not an expert on this. It may be possible to simplify the solution even further.
{% for variable in variables %}
<div class="range-wrap">
<label class="slider-label" for="{{variable.name}}">{{ variable.name }}</label>
<output class="bubble"></output>
<input
type="range"
class="range"
id="{{ variable.name }}"
name="{{ variable.name }}"
value="{{ variable.sample_value }}"
list="{{ variable.name }}-list"
min="{{ variable.min }}"
max="{{ variable.max }}"
step="{{ variable.step }}"
>
<div class="labels">
<span style="--val: {{variable.min}}; --min: {{variable.min}}; --max: {{variable.max}};">Min</span>
<span style="--val: {{variable.Q1}}; --min: {{variable.min}}; --max: {{variable.max}};">1st Q</span>
<span style="--val: {{variable.med}}; --min: {{variable.min}}; --max: {{variable.max}};">Median</span>
<span style="--val: {{variable.Q3}}; --min: {{variable.min}}; --max: {{variable.max}};">3rd Q</span>
<span style="--val: {{variable.max}}; --min: {{variable.min}}; --max: {{variable.max}};">Max</span>
</div>
<datalist id="{{ variable.name }}-list">
<option value="{{ variable.min }}" label="Min"></option>
<option value="{{ variable.Q1 }}" label="1st Q"></option>
<option value="{{ variable.med }}" label="Median"></option>
<option value="{{ variable.Q3 }}" label="3rd Q"></option>
<option value="{{ variable.max }}" label="Max"></option>
</datalist>
</div>
{% endfor %}
.range-wrap {
position: relative;
width: 500px;
margin: 0 auto 3rem;
}
.slider-label {
position: relative;
display: block;
margin: 0 auto 3rem;
}
.labels {
position:relative;
width: 500px;
}
.labels span {
padding: 0;
position: absolute;
left: calc(10px + (500px - 20px) /(var(--max) - var(--min)) * (var(--val) - var(--min)));
transform: translateX(-50%);
}
.range {
width: 500px;
}
.bubble {
background: rgb(12, 83, 163);
color: white;
padding: 4px 12px;
position: absolute;
border-radius: 4px;
left: 50%;
bottom: 32px;
transform: translateX(-50%);
}
.bubble::after {
content: "";
position: absolute;
width: 2px;
height: 2px;
background: rgb(176, 167, 218);
bottom: -2px;
left: 50%;
}
body {
margin: 2rem;
}