Open demo

CSS code

HTML
<div><span class="lbl">Tick marks and labels</span><div class="card">
<div class="tk">
  <div class="tk-head"><label for="tk-zoom">Page zoom</label><output for="tk-zoom">125%</output></div>
  <input type="range" id="tk-zoom" min="50" max="200" step="25" value="125" list="tk-zoom-stops" data-suffix="%">
  <datalist id="tk-zoom-stops">
    <option value="50"></option><option value="75"></option><option value="100"></option><option value="125"></option>
    <option value="150"></option><option value="175"></option><option value="200"></option>
  </datalist>
  <div class="tk-ticks" aria-hidden="true"></div>
  <div class="tk-labels" aria-hidden="true"><span>50%</span><span>75%</span><span>100%</span><span>125%</span><span>150%</span><span>175%</span><span>200%</span></div>
</div></div></div>

<div><span class="lbl">Ruler with major and minor ticks</span><div class="card">
<div class="tk ruler">
  <div class="tk-head"><label for="tk-temp">Target temperature</label><output for="tk-temp">21.5 °C</output></div>
  <input type="range" id="tk-temp" min="16" max="28" step="0.5" value="21.5" list="tk-temp-stops" data-suffix=" °C">
  <datalist id="tk-temp-stops">
    <option value="16"></option><option value="18"></option><option value="20"></option><option value="22"></option>
    <option value="24"></option><option value="26"></option><option value="28"></option>
  </datalist>
  <div class="tk-ticks" aria-hidden="true"></div>
  <div class="tk-labels" aria-hidden="true"><span>16°</span><span>18°</span><span>20°</span><span>22°</span><span>24°</span><span>26°</span><span>28°</span></div>
</div></div></div>

<div><span class="lbl">Step dots in the track</span><div class="card">
<div class="tk dots">
  <div class="tk-head"><label for="tk-discount">Loyalty discount</label><output for="tk-discount">20%</output></div>
  <input type="range" id="tk-discount" min="0" max="50" step="10" value="20" list="tk-discount-stops" data-suffix="%">
  <datalist id="tk-discount-stops">
    <option value="0"></option><option value="10"></option><option value="20"></option>
    <option value="30"></option><option value="40"></option><option value="50"></option>
  </datalist>
  <div class="tk-labels" aria-hidden="true"><span>0%</span><span>10%</span><span>20%</span><span>30%</span><span>40%</span><span>50%</span></div>
</div></div></div>
CSS
/* 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 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Tick marks and labels</span><div class="card demo-card"><div class="card-body p-4">
<div class="tk">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="tk-zoom">Page zoom</label><output class="fw-bold" for="tk-zoom">125%</output></div>
  <input type="range" class="form-range d-block" id="tk-zoom" min="50" max="200" step="25" value="125" list="tk-zoom-stops" data-suffix="%">
  <datalist id="tk-zoom-stops">
    <option value="50"></option><option value="75"></option><option value="100"></option><option value="125"></option>
    <option value="150"></option><option value="175"></option><option value="200"></option>
  </datalist>
  <div class="tk-ticks" aria-hidden="true"></div>
  <div class="tk-labels d-flex justify-content-between" aria-hidden="true"><span>50%</span><span>75%</span><span>100%</span><span>125%</span><span>150%</span><span>175%</span><span>200%</span></div>
</div></div></div></div>

<div><span class="lbl">Ruler with major and minor ticks</span><div class="card demo-card"><div class="card-body p-4">
<div class="tk ruler">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="tk-temp">Target temperature</label><output class="fw-bold" for="tk-temp">21.5 °C</output></div>
  <input type="range" class="form-range d-block" id="tk-temp" min="16" max="28" step="0.5" value="21.5" list="tk-temp-stops" data-suffix=" °C">
  <datalist id="tk-temp-stops">
    <option value="16"></option><option value="18"></option><option value="20"></option><option value="22"></option>
    <option value="24"></option><option value="26"></option><option value="28"></option>
  </datalist>
  <div class="tk-ticks" aria-hidden="true"></div>
  <div class="tk-labels d-flex justify-content-between" aria-hidden="true"><span>16°</span><span>18°</span><span>20°</span><span>22°</span><span>24°</span><span>26°</span><span>28°</span></div>
</div></div></div></div>

<div><span class="lbl">Step dots in the track</span><div class="card demo-card"><div class="card-body p-4">
<div class="tk dots">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="tk-discount">Loyalty discount</label><output class="fw-bold" for="tk-discount">20%</output></div>
  <input type="range" class="form-range d-block" id="tk-discount" min="0" max="50" step="10" value="20" list="tk-discount-stops" data-suffix="%">
  <datalist id="tk-discount-stops">
    <option value="0"></option><option value="10"></option><option value="20"></option>
    <option value="30"></option><option value="40"></option><option value="50"></option>
  </datalist>
  <div class="tk-labels d-flex justify-content-between" aria-hidden="true"><span>0%</span><span>10%</span><span>20%</span><span>30%</span><span>40%</span><span>50%</span></div>
</div></div></div></div>
CSS
/* 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();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#56657a] uppercase">Tick marks and labels</span><div class="rounded-2xl border border-[#dde7e5] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="tk grid gap-3 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="tk-zoom" class="text-[.95rem] font-semibold">Page zoom</label><output for="tk-zoom" class="text-[.95rem] font-bold text-tk-accent tabular-nums">125%</output></div>
  <input type="range" id="tk-zoom" min="50" max="200" step="25" value="125" list="tk-zoom-stops" data-suffix="%" class="block h-5 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--color-tk-accent)_0_calc(10px_+_(100%_-_20px)_*_var(--p)),var(--color-tk-track)_0)] [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-tk-accent)_0_calc(10px_+_(100%_-_20px)_*_var(--p)),var(--color-tk-track)_0)] [&::-webkit-slider-thumb]:-mt-[7px] [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-3 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-tk-accent [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.35)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-3 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-tk-accent [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.35)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tk-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tk-accent)]">
  <datalist id="tk-zoom-stops">
    <option value="50"></option><option value="75"></option><option value="100"></option><option value="125"></option>
    <option value="150"></option><option value="175"></option><option value="200"></option>
  </datalist>
  <div class="-mt-1 h-2 [background:linear-gradient(to_right,var(--color-tk-track)_0_2px,transparent_0)_9px_0/calc((100%_-_20px)/6)_100%_repeat-x]" aria-hidden="true"></div>
  <div class="mx-[10px] -mt-1 flex justify-between text-[.78rem] text-[#475569] tabular-nums *:flex *:w-0 *:justify-center *:whitespace-nowrap" aria-hidden="true"><span>50%</span><span>75%</span><span>100%</span><span>125%</span><span>150%</span><span>175%</span><span>200%</span></div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#56657a] uppercase">Ruler with major and minor ticks</span><div class="rounded-2xl border border-[#dde7e5] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="tk grid gap-3 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="tk-temp" class="text-[.95rem] font-semibold">Target temperature</label><output for="tk-temp" class="text-[.95rem] font-bold text-tk-accent tabular-nums">21.5 °C</output></div>
  <input type="range" id="tk-temp" min="16" max="28" step="0.5" value="21.5" list="tk-temp-stops" data-suffix=" °C" class="block h-[26px] w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--color-tk-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--p)),var(--color-tk-track)_0)] [&::-moz-range-track]:h-1 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-tk-accent)_0_calc(7px_+_(100%_-_14px)_*_var(--p)),var(--color-tk-track)_0)] [&::-webkit-slider-thumb]:-mt-[11px] [&::-webkit-slider-thumb]:h-[26px] [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-[7px] [&::-webkit-slider-thumb]:bg-tk-accent [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.35)] [&::-moz-range-thumb]:h-[26px] [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:rounded-[7px] [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-tk-accent [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.35)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tk-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tk-accent)]">
  <datalist id="tk-temp-stops">
    <option value="16"></option><option value="18"></option><option value="20"></option><option value="22"></option>
    <option value="24"></option><option value="26"></option><option value="28"></option>
  </datalist>
  <div class="-mt-1 h-3 [background:linear-gradient(to_right,var(--color-tk-track)_0_2px,transparent_0)_6px_0/calc((100%_-_14px)/6)_12px_repeat-x,linear-gradient(to_right,var(--color-tk-track)_0_1px,transparent_0)_6.5px_0/calc((100%_-_14px)/12)_6px_repeat-x]" aria-hidden="true"></div>
  <div class="mx-[7px] -mt-1 flex justify-between text-[.78rem] text-[#475569] tabular-nums *:flex *:w-0 *:justify-center *:whitespace-nowrap" aria-hidden="true"><span>16°</span><span>18°</span><span>20°</span><span>22°</span><span>24°</span><span>26°</span><span>28°</span></div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#56657a] uppercase">Step dots in the track</span><div class="rounded-2xl border border-[#dde7e5] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="tk grid gap-3 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="tk-discount" class="text-[.95rem] font-semibold">Loyalty discount</label><output for="tk-discount" class="text-[.95rem] font-bold text-tk-accent tabular-nums">20%</output></div>
  <input type="range" id="tk-discount" min="0" max="50" step="10" value="20" list="tk-discount-stops" data-suffix="%" class="block h-6 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:[background:radial-gradient(circle_at_2px_50%,#fff_0_1.6px,transparent_2.2px)_10px_0/calc((100%_-_24px)/5)_100%_repeat-x,linear-gradient(to_right,var(--color-tk-accent)_0_calc(12px_+_(100%_-_24px)_*_var(--p)),var(--color-tk-track)_0)] [&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:[background:radial-gradient(circle_at_2px_50%,#fff_0_1.6px,transparent_2.2px)_10px_0/calc((100%_-_24px)/5)_100%_repeat-x,linear-gradient(to_right,var(--color-tk-accent)_0_calc(12px_+_(100%_-_24px)_*_var(--p)),var(--color-tk-track)_0)] [&::-webkit-slider-thumb]:-mt-2 [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-6 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-4 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-tk-accent [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.35)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-6 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-4 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-tk-accent [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.35)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tk-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-tk-accent)]">
  <datalist id="tk-discount-stops">
    <option value="0"></option><option value="10"></option><option value="20"></option>
    <option value="30"></option><option value="40"></option><option value="50"></option>
  </datalist>
  <div class="mx-3 -mt-1 flex justify-between text-[.78rem] text-[#475569] tabular-nums *:flex *:w-0 *:justify-center *:whitespace-nowrap" aria-hidden="true"><span>0%</span><span>10%</span><span>20%</span><span>30%</span><span>40%</span><span>50%</span></div>
</div></div></div>
CSS
@theme {
  --color-tk-accent: #0f766e;
  --color-tk-track: #8391a7;
}
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();
});

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.