Open demo

CSS code

HTML
<div><span class="lbl">Thin</span><div class="card">
<div class="rs">
  <div class="rs-head"><label for="rs-opacity">Layer opacity</label><output for="rs-opacity">64%</output></div>
  <input type="range" id="rs-opacity" min="0" max="100" value="64" data-suffix="%">
  <div class="rs-scale" aria-hidden="true"><span>0%</span><span>100%</span></div>
</div></div></div>

<div><span class="lbl">Thick</span><div class="card">
<div class="rs thick">
  <div class="rs-head"><label for="rs-storage">Storage limit</label><output for="rs-storage">120 GB</output></div>
  <input type="range" id="rs-storage" min="10" max="200" step="10" value="120" data-suffix=" GB">
</div></div></div>

<div><span class="lbl">Value bubble</span><div class="card">
<div class="rs bubble">
  <div class="rs-head"><label for="rs-budget">Monthly ad budget</label></div>
  <div class="rs-track">
    <output for="rs-budget">$1,850</output>
    <input type="range" id="rs-budget" min="0" max="5000" step="50" value="1850" data-prefix="$">
  </div>
</div></div></div>
CSS
/* Basic range sliders: a real <input type="range"> styled through the WebKit and Firefox pseudo-elements.
   --p (0 to 1) is set by the script below and drives the filled part of the track and the value bubble. */
.rs{--rs-accent:#4f46e5;--rs-track:#8391a7;--rs-thumb:18px;--rs-h:4px;--p:0;display:grid;gap:14px}
.rs-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
.rs-head label{font-size:.95rem;font-weight:600;color:#1e293b}
.rs-head output{font-size:.95rem;font-weight:700;color:var(--rs-accent);font-variant-numeric:tabular-nums}
.rs input[type="range"]{-webkit-appearance:none;appearance:none;display:block;width:100%;height:var(--rs-thumb);margin:0;background:none;cursor:pointer}
.rs input[type="range"]:focus-visible{outline:none}
.rs input[type="range"]::-webkit-slider-runnable-track{height:var(--rs-h);border-radius:999px;
  background:linear-gradient(to right,var(--rs-accent) 0 calc(var(--rs-thumb) / 2 + (100% - var(--rs-thumb)) * var(--p)),var(--rs-track) 0)}
.rs input[type="range"]::-moz-range-track{height:var(--rs-h);border-radius:999px;
  background:linear-gradient(to right,var(--rs-accent) 0 calc(var(--rs-thumb) / 2 + (100% - var(--rs-thumb)) * var(--p)),var(--rs-track) 0)}
.rs input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;box-sizing:border-box;
  width:var(--rs-thumb);height:var(--rs-thumb);margin-top:calc((var(--rs-h) - var(--rs-thumb)) / 2);
  border:2px solid var(--rs-accent);border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.25)}
.rs input[type="range"]::-moz-range-thumb{box-sizing:border-box;width:var(--rs-thumb);height:var(--rs-thumb);
  border:2px solid var(--rs-accent);border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.25)}
.rs input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--rs-accent)}
.rs input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--rs-accent)}
.rs-scale{display:flex;justify-content:space-between;margin-top:-4px;font-size:.8rem;color:#64748b;font-variant-numeric:tabular-nums}
/* thick: a 12px track and a solid thumb with a white rim */
.rs.thick{--rs-thumb:28px;--rs-h:12px}
.rs.thick input[type="range"]::-webkit-slider-thumb{border:4px solid #fff;background:var(--rs-accent);box-shadow:0 2px 6px rgba(15,23,42,.3)}
.rs.thick input[type="range"]::-moz-range-thumb{border:4px solid #fff;background:var(--rs-accent);box-shadow:0 2px 6px rgba(15,23,42,.3)}
.rs.thick input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--rs-accent)}
.rs.thick input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--rs-accent)}
/* value bubble: the <output> rides above the thumb */
.rs.bubble{--rs-thumb:22px;--rs-h:6px}
.rs-track{position:relative;padding-top:44px}
.rs-track output{position:absolute;top:0;left:calc(var(--rs-thumb) / 2 + (100% - var(--rs-thumb)) * var(--p));transform:translateX(-50%);
  padding:5px 10px;border-radius:8px;background:var(--rs-accent);color:#fff;font-size:.85rem;font-weight:700;
  font-variant-numeric:tabular-nums;white-space:nowrap}
.rs-track output::after{content:"";position:absolute;left:50%;bottom:-5px;width:10px;height:10px;margin-left:-5px;
  background:inherit;border-radius:0 0 2px 0;transform:rotate(45deg)}
JavaScript
// Keeps each slider's filled track (--p) and its <output> in step with the value.
document.querySelectorAll('.rs input[type="range"]').forEach(function (range) {
  var field = range.closest('.rs');
  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">Thin</span><div class="card demo-card"><div class="card-body p-4">
<div class="rs">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="rs-opacity">Layer opacity</label><output class="fw-bold" for="rs-opacity">64%</output></div>
  <input type="range" class="form-range d-block" id="rs-opacity" min="0" max="100" value="64" data-suffix="%">
  <div class="d-flex justify-content-between small text-secondary rs-scale" aria-hidden="true"><span>0%</span><span>100%</span></div>
</div></div></div></div>

<div><span class="lbl">Thick</span><div class="card demo-card"><div class="card-body p-4">
<div class="rs thick">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="rs-storage">Storage limit</label><output class="fw-bold" for="rs-storage">120 GB</output></div>
  <input type="range" class="form-range d-block" id="rs-storage" min="10" max="200" step="10" value="120" data-suffix=" GB">
</div></div></div></div>

<div><span class="lbl">Value bubble</span><div class="card demo-card"><div class="card-body p-4">
<div class="rs bubble">
  <label class="form-label fw-semibold mb-0" for="rs-budget">Monthly ad budget</label>
  <div class="rs-track">
    <output class="badge rs-bubble" for="rs-budget">$1,850</output>
    <input type="range" class="form-range d-block" id="rs-budget" min="0" max="5000" step="50" value="1850" data-prefix="$">
  </div>
</div></div></div></div>
CSS
/* Bootstrap .form-range, restyled. --p (0 to 1) is set by the script below and drives the fill and the bubble. */
.rs{--rs-accent:#4f46e5;--rs-track:#8391a7;--rs-thumb:18px;--rs-h:4px;--p:0;display:grid;gap:14px}
.rs .form-label{font-size:.95rem;color:#1e293b}
.rs output{font-size:.95rem;color:var(--rs-accent);font-variant-numeric:tabular-nums}
.rs .form-range{height:var(--rs-thumb)}
.rs .rs-scale{margin-top:-4px}
.rs .form-range::-webkit-slider-runnable-track{height:var(--rs-h);border-radius:999px;
  background:linear-gradient(to right,var(--rs-accent) 0 calc(var(--rs-thumb) / 2 + (100% - var(--rs-thumb)) * var(--p)),var(--rs-track) 0)}
.rs .form-range::-moz-range-track{height:var(--rs-h);border-radius:999px;
  background:linear-gradient(to right,var(--rs-accent) 0 calc(var(--rs-thumb) / 2 + (100% - var(--rs-thumb)) * var(--p)),var(--rs-track) 0)}
.rs .form-range::-webkit-slider-thumb{box-sizing:border-box;width:var(--rs-thumb);height:var(--rs-thumb);
  margin-top:calc((var(--rs-h) - var(--rs-thumb)) / 2);border:2px solid var(--rs-accent);border-radius:50%;
  background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.25)}
.rs .form-range::-moz-range-thumb{box-sizing:border-box;width:var(--rs-thumb);height:var(--rs-thumb);
  border:2px solid var(--rs-accent);border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.25)}
.rs .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--rs-accent)}
.rs .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--rs-accent)}
/* thick */
.rs.thick{--rs-thumb:28px;--rs-h:12px}
.rs.thick .form-range::-webkit-slider-thumb{border:4px solid #fff;background:var(--rs-accent);box-shadow:0 2px 6px rgba(15,23,42,.3)}
.rs.thick .form-range::-moz-range-thumb{border:4px solid #fff;background:var(--rs-accent);box-shadow:0 2px 6px rgba(15,23,42,.3)}
.rs.thick .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px var(--rs-accent)}
.rs.thick .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px var(--rs-accent)}
/* value bubble: a .badge that rides above the thumb */
.rs .rs-track{position:relative;padding-top:44px}
.rs.bubble{--rs-thumb:22px;--rs-h:6px}
.rs .rs-bubble{position:absolute;top:0;left:calc(var(--rs-thumb) / 2 + (100% - var(--rs-thumb)) * var(--p));transform:translateX(-50%);
  --bs-badge-padding-x:10px;--bs-badge-padding-y:5px;--bs-badge-font-size:.85rem;--bs-badge-border-radius:8px;
  background:var(--rs-accent);color:#fff;font-size:.85rem}
.rs .rs-bubble::after{content:"";position:absolute;left:50%;bottom:-5px;width:10px;height:10px;margin-left:-5px;
  background:inherit;border-radius:0 0 2px 0;transform:rotate(45deg)}
JavaScript
// Keeps each slider's filled track (--p) and its <output> in step with the value.
document.querySelectorAll('.rs input[type="range"]').forEach(function (range) {
  var field = range.closest('.rs');
  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-[#5b6b82] uppercase">Thin</span><div class="rounded-2xl border border-[#e2e8f0] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="rs grid gap-3.5 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="rs-opacity" class="text-[.95rem] font-semibold">Layer opacity</label><output for="rs-opacity" class="text-[.95rem] font-bold text-rs-accent tabular-nums">64%</output></div>
  <input type="range" id="rs-opacity" min="0" max="100" value="64" data-suffix="%" class="block h-[18px] 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-rs-accent)_0_calc(9px_+_(100%_-_18px)_*_var(--p)),var(--color-rs-track)_0)] [&::-moz-range-track]:h-1 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-rs-accent)_0_calc(9px_+_(100%_-_18px)_*_var(--p)),var(--color-rs-track)_0)] [&::-webkit-slider-thumb]:-mt-[7px] [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-[18px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-rs-accent [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.25)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[18px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-rs-accent [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.25)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-rs-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-rs-accent)]">
  <div class="-mt-1 flex justify-between text-[.8rem] text-[#64748b] tabular-nums" aria-hidden="true"><span>0%</span><span>100%</span></div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6b82] uppercase">Thick</span><div class="rounded-2xl border border-[#e2e8f0] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="rs grid gap-3.5 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="rs-storage" class="text-[.95rem] font-semibold">Storage limit</label><output for="rs-storage" class="text-[.95rem] font-bold text-rs-accent tabular-nums">120 GB</output></div>
  <input type="range" id="rs-storage" min="10" max="200" step="10" value="120" data-suffix=" GB" class="block h-7 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-3 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--color-rs-accent)_0_calc(14px_+_(100%_-_28px)_*_var(--p)),var(--color-rs-track)_0)] [&::-moz-range-track]:h-3 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-rs-accent)_0_calc(14px_+_(100%_-_28px)_*_var(--p)),var(--color-rs-track)_0)] [&::-webkit-slider-thumb]:-mt-2 [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-7 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-4 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-rs-accent [&::-webkit-slider-thumb]:shadow-[0_2px_6px_rgba(15,23,42,.3)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-7 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-4 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-rs-accent [&::-moz-range-thumb]:shadow-[0_2px_6px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_var(--color-rs-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_var(--color-rs-accent)]">
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5b6b82] uppercase">Value bubble</span><div class="rounded-2xl border border-[#e2e8f0] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="rs grid gap-3.5 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="rs-budget" class="text-[.95rem] font-semibold">Monthly ad budget</label></div>
  <div class="relative pt-11">
    <output for="rs-budget" class="absolute top-0 left-[calc(11px_+_(100%_-_22px)_*_var(--p))] -translate-x-1/2 rounded-lg bg-rs-accent px-2.5 py-[5px] text-[.85rem] font-bold whitespace-nowrap text-white tabular-nums after:absolute after:-bottom-[5px] after:left-1/2 after:-ml-[5px] after:size-2.5 after:rotate-45 after:rounded-br-[2px] after:bg-inherit after:content-['']">$1,850</output>
    <input type="range" id="rs-budget" min="0" max="5000" step="50" value="1850" data-prefix="$" class="block h-[22px] 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-rs-accent)_0_calc(11px_+_(100%_-_22px)_*_var(--p)),var(--color-rs-track)_0)] [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-rs-accent)_0_calc(11px_+_(100%_-_22px)_*_var(--p)),var(--color-rs-track)_0)] [&::-webkit-slider-thumb]:-mt-2 [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-[22px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-rs-accent [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.25)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[22px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-rs-accent [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-[0_1px_3px_rgba(15,23,42,.25)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-rs-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-rs-accent)]">
  </div>
</div></div></div>
CSS
@theme {
  --color-rs-accent: #4f46e5;
  --color-rs-track: #8391a7;
}
JavaScript
// Keeps each slider's filled track (--p) and its <output> in step with the value.
document.querySelectorAll('.rs input[type="range"]').forEach(function (range) {
  var field = range.closest('.rs');
  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 everyday range sliders that cover most settings pages and filters. Thin pairs a 4 pixel track with an 18 pixel ringed thumb and a 0 to 100 percent scale underneath, for layer opacity or any percentage. Thick uses a 12 pixel track and a solid 28 pixel thumb with a white rim, a good fit for touch screens and for a single important control such as a storage limit. Value bubble moves the readout into a small tooltip that rides above the thumb, useful for money amounts like a monthly ad budget where the number matters more than the position. Every slider is a real input type=”range” styled through the ::-webkit-slider-runnable-track, ::-webkit-slider-thumb, ::-moz-range-track and ::-moz-range-thumb pseudo-elements, so dragging, clicking the track, arrow keys, Page Up and Down, Home and End and form submission all come from the browser. A dozen lines of JavaScript set one custom property, –p, from 0 to 1, which paints the filled part of the track and positions the bubble, and they copy the formatted value into the visible output and into aria-valuetext.

What’s included

  • Thin, thick and value-bubble variants
  • Real input type="range" styled for WebKit and Firefox
  • Filled track driven by one --p custom property
  • Formatted value in an output element and in aria-valuetext
  • Unfilled track at 3:1 against the card
  • Plain CSS, Bootstrap .form-range and Tailwind versions of the same design

Accessibility

Each slider has a visible label tied to it with for and id, and the script writes the formatted value, such as $1,850 or 120 GB, into aria-valuetext so screen readers announce the unit rather than a bare number. Arrow keys, Page Up, Page Down, Home and End work as on any range input. Keyboard focus draws a two-colour ring around the thumb, and nothing animates.

Customise it

Change --rs-accent, --rs-track, --rs-thumb and --rs-h on .rs in the plain CSS and Bootstrap versions. In Tailwind edit the --color-rs-accent and --color-rs-track theme colours, and the size-, h- and -mt- utilities on the thumb and track variants.