/* Range sliders with tick marks. The stops live in a <datalist>; the visible ticks and labels are drawn to line up
with the thumb centre: first stop at half a thumb from the left edge, last at half a thumb from the right.
--n is the number of intervals between ticks, --p (0 to 1) is set by the script below. */
.tk{--tk-accent:#0f766e;--tk-track:#8391a7;--tk-thumb:20px;--tk-h:6px;--n:6;--p:0;display:grid;gap:12px}
.tk-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
.tk-head label{font-size:.95rem;font-weight:600;color:#1e293b}
.tk-head output{font-size:.95rem;font-weight:700;color:var(--tk-accent);font-variant-numeric:tabular-nums}
.tk input[type="range"]{-webkit-appearance:none;appearance:none;display:block;width:100%;height:var(--tk-thumb);margin:0;background:none;cursor:pointer}
.tk input[type="range"]:focus-visible{outline:none}
.tk input[type="range"]::-webkit-slider-runnable-track{height:var(--tk-h);border-radius:999px;
background:linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk input[type="range"]::-moz-range-track{height:var(--tk-h);border-radius:999px;
background:linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;box-sizing:border-box;
width:var(--tk-thumb);height:var(--tk-thumb);margin-top:calc((var(--tk-h) - var(--tk-thumb)) / 2);
border:3px solid #fff;border-radius:50%;background:var(--tk-accent);box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk input[type="range"]::-moz-range-thumb{box-sizing:border-box;width:var(--tk-thumb);height:var(--tk-thumb);
border:3px solid #fff;border-radius:50%;background:var(--tk-accent);box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
.tk input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
/* tick row: one 2px line per stop */
.tk-ticks{height:8px;margin-top:-4px;
background:linear-gradient(to right,var(--tk-track) 0 2px,transparent 0) calc(var(--tk-thumb) / 2 - 1px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 100% repeat-x}
/* label row: zero-width cells spread between the first and last thumb centre, text centred on each */
.tk-labels{display:flex;justify-content:space-between;margin:-4px calc(var(--tk-thumb) / 2) 0;
font-size:.78rem;color:#475569;font-variant-numeric:tabular-nums}
.tk-labels span{width:0;display:flex;justify-content:center;white-space:nowrap}
/* ruler: minor ticks every interval, taller major ticks every second one */
.tk.ruler{--tk-thumb:14px;--tk-h:4px;--n:12}
.tk.ruler input[type="range"]{height:26px}
.tk.ruler input[type="range"]::-webkit-slider-thumb{width:14px;height:26px;margin-top:-11px;border:0;border-radius:7px;box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk.ruler input[type="range"]::-moz-range-thumb{width:14px;height:26px;border:0;border-radius:7px;box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk.ruler input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
.tk.ruler input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
.tk.ruler .tk-ticks{height:12px;background:
linear-gradient(to right,var(--tk-track) 0 2px,transparent 0) calc(var(--tk-thumb) / 2 - 1px) 0 / calc((100% - var(--tk-thumb)) / var(--n) * 2) 12px repeat-x,
linear-gradient(to right,var(--tk-track) 0 1px,transparent 0) calc(var(--tk-thumb) / 2 - .5px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 6px repeat-x}
/* step dots drawn inside a thicker track */
.tk.dots{--tk-thumb:24px;--tk-h:8px;--n:5}
.tk.dots input[type="range"]::-webkit-slider-runnable-track{background:
radial-gradient(circle at 2px 50%,#fff 0 1.6px,transparent 2.2px) calc(var(--tk-thumb) / 2 - 2px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 100% repeat-x,
linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk.dots input[type="range"]::-moz-range-track{background:
radial-gradient(circle at 2px 50%,#fff 0 1.6px,transparent 2.2px) calc(var(--tk-thumb) / 2 - 2px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 100% repeat-x,
linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk.dots input[type="range"]::-webkit-slider-thumb{border-width:4px}
.tk.dots input[type="range"]::-moz-range-thumb{border-width:4px}
JavaScript
// Keeps each slider's filled track (--p) and its <output> in step with the value.
document.querySelectorAll('.tk input[type="range"]').forEach(function (range) {
var field = range.closest('.tk');
var out = field.querySelector('output');
function sync() {
var text = (range.dataset.prefix || '') + Number(range.value).toLocaleString('en-US') + (range.dataset.suffix || '');
field.style.setProperty('--p', (range.value - range.min) / (range.max - range.min));
if (out) out.textContent = text;
range.setAttribute('aria-valuetext', text);
}
range.addEventListener('input', sync);
sync();
});
/* Bootstrap .form-range with tick marks. The stops live in a <datalist>; the visible ticks and labels are drawn to line up
with the thumb centre: first stop at half a thumb from the left edge, last at half a thumb from the right.
--n is the number of intervals between ticks, --p (0 to 1) is set by the script below. */
.tk{--tk-accent:#0f766e;--tk-track:#8391a7;--tk-thumb:20px;--tk-h:6px;--n:6;--p:0;display:grid;gap:12px}
.tk .form-label{font-size:.95rem;color:#1e293b}
.tk output{font-size:.95rem;color:var(--tk-accent);font-variant-numeric:tabular-nums}
.tk .form-range{height:var(--tk-thumb)}
.tk .form-range::-webkit-slider-runnable-track{height:var(--tk-h);border-radius:999px;
background:linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk .form-range::-moz-range-track{height:var(--tk-h);border-radius:999px;
background:linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk .form-range::-webkit-slider-thumb{box-sizing:border-box;
width:var(--tk-thumb);height:var(--tk-thumb);margin-top:calc((var(--tk-h) - var(--tk-thumb)) / 2);
border:3px solid #fff;border-radius:50%;background:var(--tk-accent);box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk .form-range::-moz-range-thumb{box-sizing:border-box;width:var(--tk-thumb);height:var(--tk-thumb);
border:3px solid #fff;border-radius:50%;background:var(--tk-accent);box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
.tk .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
/* tick row: one 2px line per stop */
.tk-ticks{height:8px;margin-top:-4px;
background:linear-gradient(to right,var(--tk-track) 0 2px,transparent 0) calc(var(--tk-thumb) / 2 - 1px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 100% repeat-x}
/* label row: zero-width cells spread between the first and last thumb centre, text centred on each */
.tk-labels{margin:-4px calc(var(--tk-thumb) / 2) 0;
font-size:.78rem;color:#475569;font-variant-numeric:tabular-nums}
.tk-labels span{width:0;display:flex;justify-content:center;white-space:nowrap}
/* ruler: minor ticks every interval, taller major ticks every second one */
.tk.ruler{--tk-thumb:14px;--tk-h:4px;--n:12}
.tk.ruler .form-range{height:26px}
.tk.ruler .form-range::-webkit-slider-thumb{width:14px;height:26px;margin-top:-11px;border:0;border-radius:7px;box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk.ruler .form-range::-moz-range-thumb{width:14px;height:26px;border:0;border-radius:7px;box-shadow:0 1px 4px rgba(15,23,42,.35)}
.tk.ruler .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
.tk.ruler .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--tk-accent)}
.tk.ruler .tk-ticks{height:12px;background:
linear-gradient(to right,var(--tk-track) 0 2px,transparent 0) calc(var(--tk-thumb) / 2 - 1px) 0 / calc((100% - var(--tk-thumb)) / var(--n) * 2) 12px repeat-x,
linear-gradient(to right,var(--tk-track) 0 1px,transparent 0) calc(var(--tk-thumb) / 2 - .5px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 6px repeat-x}
/* step dots drawn inside a thicker track */
.tk.dots{--tk-thumb:24px;--tk-h:8px;--n:5}
.tk.dots .form-range::-webkit-slider-runnable-track{background:
radial-gradient(circle at 2px 50%,#fff 0 1.6px,transparent 2.2px) calc(var(--tk-thumb) / 2 - 2px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 100% repeat-x,
linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk.dots .form-range::-moz-range-track{background:
radial-gradient(circle at 2px 50%,#fff 0 1.6px,transparent 2.2px) calc(var(--tk-thumb) / 2 - 2px) 0 / calc((100% - var(--tk-thumb)) / var(--n)) 100% repeat-x,
linear-gradient(to right,var(--tk-accent) 0 calc(var(--tk-thumb) / 2 + (100% - var(--tk-thumb)) * var(--p)),var(--tk-track) 0)}
.tk.dots .form-range::-webkit-slider-thumb{border-width:4px}
.tk.dots .form-range::-moz-range-thumb{border-width:4px}
JavaScript
// Keeps each slider's filled track (--p) and its <output> in step with the value.
document.querySelectorAll('.tk .form-range').forEach(function (range) {
var field = range.closest('.tk');
var out = field.querySelector('output');
function sync() {
var text = (range.dataset.prefix || '') + Number(range.value).toLocaleString('en-US') + (range.dataset.suffix || '');
field.style.setProperty('--p', (range.value - range.min) / (range.max - range.min));
if (out) out.textContent = text;
range.setAttribute('aria-valuetext', text);
}
range.addEventListener('input', sync);
sync();
});
// Keeps each slider's filled track (--p) and its <output> in step with the value.
document.querySelectorAll('.tk input[type="range"]').forEach(function (range) {
var field = range.closest('.tk');
var out = field.querySelector('output');
function sync() {
var text = (range.dataset.prefix || '') + Number(range.value).toLocaleString('en-US') + (range.dataset.suffix || '');
field.style.setProperty('--p', (range.value - range.min) / (range.max - range.min));
if (out) out.textContent = text;
range.setAttribute('aria-valuetext', text);
}
range.addEventListener('input', sync);
sync();
});
About this set
Three sliders that show where the stops are. Tick marks and labels puts a short line and a label under every step of a page zoom control from 50 to 200 percent. Ruler with major and minor ticks draws a thermostat scale with a tall tick every 2 degrees, a short one every degree and a pill-shaped thumb, for fine values with a step of 0.5. Step dots in the track prints small white dots inside a thicker track at each of six discount levels, the way Material style discrete sliders do. Use ticks when people need to aim for particular values and dots when the steps are few and even. The stops are listed in a datalist linked with the list attribute, so the semantics stay in the markup, but browsers do not draw datalist ticks once a range input is restyled, so the visible ticks are CSS gradients and the labels are a flex row of zero-width cells. Both start and end half a thumb in from the edges, which is exactly where the thumb centre sits at the minimum and maximum, so every tick lines up with the thumb at every width. A short script keeps the filled track and the readout current.
What’s included
Tick row, ruler with major and minor ticks, and dots inside the track
Stops declared in a datalist with the list attribute
Ticks and labels aligned with the thumb centre at any width
Tick spacing set by one --n custom property
Formatted value such as 21.5 °C in the output and aria-valuetext
Accessibility
Each slider has a visible label and an output linked with for. The script puts the value with its unit into aria-valuetext, and the drawn ticks and scale labels are aria-hidden because they repeat what the input already exposes. Keyboard steps follow the step attribute, and focus shows a ring around the thumb.
Customise it
Set --tk-accent and --tk-track for colour, --tk-thumb and --tk-h for size and --n for the number of intervals between ticks on .tk (plain CSS and Bootstrap). In Tailwind change the --color-tk-* theme colours and the divisor in the calc() of the tick background.