Open demo

CSS code

HTML
<div><span class="lbl">Rating with stars</span><div class="card">
<div class="st">
  <div class="st-head"><label for="st-stay">How was your stay?</label><output for="st-stay" class="st-value">Great</output></div>
  <div class="st-stars" aria-hidden="true">
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
  </div>
  <input type="range" id="st-stay" min="1" max="5" step="1" value="4" data-labels="Poor|Fair|Good|Great|Superb">
  <div class="st-marks" aria-hidden="true"><span>Poor</span><span>Fair</span><span>Good</span><span>Great</span><span>Superb</span></div>
</div></div></div>

<div><span class="lbl">Segmented size picker</span><div class="card">
<div class="st seg">
  <div class="st-head"><label for="st-size">T-shirt size</label><output for="st-size" class="st-value">Medium</output></div>
  <div class="st-seg">
    <input type="range" id="st-size" min="0" max="5" step="1" value="2" data-labels="Extra small|Small|Medium|Large|Extra large|Double extra large">
    <div class="st-marks" aria-hidden="true"><span>XS</span><span>S</span><span>M</span><span>L</span><span>XL</span><span>XXL</span></div>
  </div>
</div></div></div>

<div><span class="lbl">Pricing by team size</span><div class="card">
<div class="st">
  <div class="st-head"><label for="st-seats">Team size</label><output for="st-seats" class="st-value">25 seats</output></div>
  <div class="st-plan"><output for="st-seats" class="st-price">$250</output><span class="st-per">per month · $10 a seat</span></div>
  <input type="range" id="st-seats" min="0" max="5" step="1" value="2" data-labels="5 seats|10 seats|25 seats|50 seats|100 seats|250 seats" data-seats="5|10|25|50|100|250" data-prices="60|110|250|450|800|1750">
  <div class="st-marks" aria-hidden="true"><span>5</span><span>10</span><span>25</span><span>50</span><span>100</span><span>250</span></div>
</div></div></div>
CSS
/* Step sliders: a real <input type="range"> with step="1" whose positions stand for words (ratings, sizes, plans).
   The script reads data-labels, writes the word to the <output> and to aria-valuetext, sets --p (0 to 1)
   and marks the matching stars and labels with data-on. */
.st{--st-accent:#6d28d9;--st-track:#8b7fb0;--st-thumb:24px;--st-h:6px;--p:0;display:grid;gap:12px}
.st-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px}
.st-head label{font-size:.95rem;font-weight:600;color:#1e1b2e}
.st-head output{font-size:.95rem;font-weight:700;color:var(--st-accent)}
.st input[type="range"]{-webkit-appearance:none;appearance:none;display:block;width:100%;height:var(--st-thumb);margin:0;background:none;cursor:pointer}
.st input[type="range"]:focus-visible{outline:none}
.st input[type="range"]::-webkit-slider-runnable-track{height:var(--st-h);border-radius:999px;
  background:linear-gradient(to right,var(--st-accent) 0 calc(var(--st-thumb) / 2 + (100% - var(--st-thumb)) * var(--p)),var(--st-track) 0)}
.st input[type="range"]::-moz-range-track{height:var(--st-h);border-radius:999px;
  background:linear-gradient(to right,var(--st-accent) 0 calc(var(--st-thumb) / 2 + (100% - var(--st-thumb)) * var(--p)),var(--st-track) 0)}
.st input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;box-sizing:border-box;
  width:var(--st-thumb);height:var(--st-thumb);margin-top:calc((var(--st-h) - var(--st-thumb)) / 2);
  border:3px solid var(--st-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(30,27,46,.3)}
.st input[type="range"]::-moz-range-thumb{box-sizing:border-box;width:var(--st-thumb);height:var(--st-thumb);
  border:3px solid var(--st-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(30,27,46,.3)}
.st input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--st-accent)}
.st input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--st-accent)}
/* rows of stars or words, one zero-width cell per stop so each centres on the thumb position */
.st-stars,.st-marks{display:flex;justify-content:space-between;margin:0 calc(var(--st-thumb) / 2)}
.st-stars > *,.st-marks > *{width:0;display:flex;justify-content:center;white-space:nowrap}
.st-stars svg{flex:none;width:26px;height:26px;fill:#c4b5fd;transition:fill .15s}
.st-stars [data-on] svg{fill:#f59e0b}
.st-marks{font-size:.8rem;color:#475569}
.st-marks [data-on]{color:var(--st-accent);font-weight:700}
/* segmented: the words sit inside a tall track and the thumb is a pill that slides under them */
.st.seg{--st-thumb:52px;--st-h:40px;--st-track:#ede9fe}
.st-seg{position:relative}
.st.seg input[type="range"]{height:40px}
.st.seg input[type="range"]::-webkit-slider-runnable-track{border-radius:12px;background:var(--st-track);box-shadow:inset 0 0 0 1px #8b7fb0}
.st.seg input[type="range"]::-moz-range-track{border-radius:12px;background:var(--st-track);box-shadow:inset 0 0 0 1px #8b7fb0}
.st.seg input[type="range"]::-webkit-slider-thumb{height:40px;margin-top:0;border:0;border-radius:12px;background:var(--st-accent);box-shadow:0 2px 6px rgba(109,40,217,.35)}
.st.seg input[type="range"]::-moz-range-thumb{height:40px;border:0;border-radius:12px;background:var(--st-accent);box-shadow:0 2px 6px rgba(109,40,217,.35)}
.st.seg input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--st-accent)}
.st.seg input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--st-accent)}
.st-seg .st-marks{position:absolute;inset:0;align-items:center;pointer-events:none;font-size:.85rem;font-weight:700;color:#3b3355}
.st-seg .st-marks [data-on]{color:#fff}
/* pricing: a big price that follows the plan */
.st-plan{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 10px}
.st-price{font-size:2.2rem;font-weight:800;letter-spacing:-.02em;color:#1e1b2e;font-variant-numeric:tabular-nums}
.st-plan span{font-size:.9rem;color:#475569}
@media (prefers-reduced-motion:reduce){.st-stars svg{transition:none}}
JavaScript
// Step sliders: maps each stop to a word, updates the readouts and aria-valuetext, and marks stars and labels.
document.querySelectorAll('.st input[type="range"]').forEach(function (range) {
  var field = range.closest('.st');
  var labels = range.dataset.labels.split('|');
  var seats = (range.dataset.seats || '').split('|'), prices = (range.dataset.prices || '').split('|');
  var stars = field.querySelectorAll('.st-stars > *'), marks = field.querySelectorAll('.st-marks > *');
  var value = field.querySelector('.st-value'), price = field.querySelector('.st-price'), per = field.querySelector('.st-per');
  function sync() {
    var i = range.value - range.min, text = labels[i];
    field.style.setProperty('--p', i / (range.max - range.min));
    stars.forEach(function (s, n) { s.toggleAttribute('data-on', n <= i); });
    marks.forEach(function (m, n) { m.toggleAttribute('data-on', n === i); });
    if (value) value.textContent = text;
    if (stars.length) text = (i + 1) + ' of ' + stars.length + ', ' + text;
    if (price) {
      price.textContent = '$' + Number(prices[i]).toLocaleString('en-US');
      per.textContent = 'per month · $' + prices[i] / seats[i] + ' a seat';
      text += ', $' + Number(prices[i]).toLocaleString('en-US') + ' per month';
    }
    range.setAttribute('aria-valuetext', text);
  }
  range.addEventListener('input', sync);
  sync();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Rating with stars</span><div class="card demo-card"><div class="card-body p-4">
<div class="st">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="st-stay">How was your stay?</label><output class="st-value fw-bold" for="st-stay">Great</output></div>
  <div class="st-stars" aria-hidden="true">
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
  </div>
  <input type="range" class="form-range" id="st-stay" min="1" max="5" step="1" value="4" data-labels="Poor|Fair|Good|Great|Superb">
  <div class="st-marks" aria-hidden="true"><span>Poor</span><span>Fair</span><span>Good</span><span>Great</span><span>Superb</span></div>
</div></div></div></div>

<div><span class="lbl">Segmented size picker</span><div class="card demo-card"><div class="card-body p-4">
<div class="st seg">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="st-size">T-shirt size</label><output class="st-value fw-bold" for="st-size">Medium</output></div>
  <div class="st-seg">
    <input type="range" class="form-range" id="st-size" min="0" max="5" step="1" value="2" data-labels="Extra small|Small|Medium|Large|Extra large|Double extra large">
    <div class="st-marks" aria-hidden="true"><span>XS</span><span>S</span><span>M</span><span>L</span><span>XL</span><span>XXL</span></div>
  </div>
</div></div></div></div>

<div><span class="lbl">Pricing by team size</span><div class="card demo-card"><div class="card-body p-4">
<div class="st">
  <div class="d-flex justify-content-between align-items-baseline gap-3"><label class="form-label fw-semibold mb-0" for="st-seats">Team size</label><output class="st-value fw-bold" for="st-seats">25 seats</output></div>
  <div class="st-plan d-flex align-items-baseline flex-wrap column-gap-2 row-gap-1"><output for="st-seats" class="st-price fw-bolder">$250</output><span class="st-per">per month · $10 a seat</span></div>
  <input type="range" class="form-range" id="st-seats" min="0" max="5" step="1" value="2" data-labels="5 seats|10 seats|25 seats|50 seats|100 seats|250 seats" data-seats="5|10|25|50|100|250" data-prices="60|110|250|450|800|1750">
  <div class="st-marks" aria-hidden="true"><span>5</span><span>10</span><span>25</span><span>50</span><span>100</span><span>250</span></div>
</div></div></div></div>
CSS
/* Step sliders: a Bootstrap .form-range with step="1" whose positions stand for words (ratings, sizes, plans).
   The script reads data-labels, writes the word to the <output> and to aria-valuetext, sets --p (0 to 1)
   and marks the matching stars and labels with data-on. */
.st{--st-accent:#6d28d9;--st-track:#8b7fb0;--st-thumb:24px;--st-h:6px;--p:0;display:grid;gap:12px}
.st .form-label{font-size:.95rem;color:#1e1b2e}
.st .st-value{font-size:.95rem;color:var(--st-accent)}
.st .form-range{height:var(--st-thumb)}
.st .form-range::-webkit-slider-runnable-track{height:var(--st-h);border-radius:999px;
  background:linear-gradient(to right,var(--st-accent) 0 calc(var(--st-thumb) / 2 + (100% - var(--st-thumb)) * var(--p)),var(--st-track) 0)}
.st .form-range::-moz-range-track{height:var(--st-h);border-radius:999px;
  background:linear-gradient(to right,var(--st-accent) 0 calc(var(--st-thumb) / 2 + (100% - var(--st-thumb)) * var(--p)),var(--st-track) 0)}
.st .form-range::-webkit-slider-thumb{box-sizing:border-box;
  width:var(--st-thumb);height:var(--st-thumb);margin-top:calc((var(--st-h) - var(--st-thumb)) / 2);
  border:3px solid var(--st-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(30,27,46,.3)}
.st .form-range::-moz-range-thumb{box-sizing:border-box;width:var(--st-thumb);height:var(--st-thumb);
  border:3px solid var(--st-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(30,27,46,.3)}
.st .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--st-accent)}
.st .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--st-accent)}
/* rows of stars or words, one zero-width cell per stop so each centres on the thumb position */
.st-stars,.st-marks{display:flex;justify-content:space-between;margin:0 calc(var(--st-thumb) / 2)}
.st-stars > *,.st-marks > *{width:0;display:flex;justify-content:center;white-space:nowrap}
.st-stars svg{flex:none;width:26px;height:26px;fill:#c4b5fd;transition:fill .15s}
.st-stars [data-on] svg{fill:#f59e0b}
.st-marks{font-size:.8rem;color:#475569}
.st-marks [data-on]{color:var(--st-accent);font-weight:700}
/* segmented: the words sit inside a tall track and the thumb is a pill that slides under them */
.st.seg{--st-thumb:52px;--st-h:40px;--st-track:#ede9fe}
.st-seg{position:relative}
.st.seg .form-range{height:40px}
.st.seg .form-range::-webkit-slider-runnable-track{border-radius:12px;background:var(--st-track);box-shadow:inset 0 0 0 1px #8b7fb0}
.st.seg .form-range::-moz-range-track{border-radius:12px;background:var(--st-track);box-shadow:inset 0 0 0 1px #8b7fb0}
.st.seg .form-range::-webkit-slider-thumb{height:40px;margin-top:0;border:0;border-radius:12px;background:var(--st-accent);box-shadow:0 2px 6px rgba(109,40,217,.35)}
.st.seg .form-range::-moz-range-thumb{height:40px;border:0;border-radius:12px;background:var(--st-accent);box-shadow:0 2px 6px rgba(109,40,217,.35)}
.st.seg .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--st-accent)}
.st.seg .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 2px #fff,0 0 0 4px var(--st-accent)}
.st-seg .st-marks{position:absolute;inset:0;align-items:center;pointer-events:none;font-size:.85rem;font-weight:700;color:#3b3355}
.st-seg .st-marks [data-on]{color:#fff}
/* pricing: a big price that follows the plan */
.st-price{font-size:2.2rem;letter-spacing:-.02em;color:#1e1b2e;font-variant-numeric:tabular-nums}
.st-plan span{font-size:.9rem;color:#475569}
@media (prefers-reduced-motion:reduce){.st-stars svg{transition:none}}
JavaScript
// Step sliders: maps each stop to a word, updates the readouts and aria-valuetext, and marks stars and labels.
document.querySelectorAll('.st .form-range').forEach(function (range) {
  var field = range.closest('.st');
  var labels = range.dataset.labels.split('|');
  var seats = (range.dataset.seats || '').split('|'), prices = (range.dataset.prices || '').split('|');
  var stars = field.querySelectorAll('.st-stars > *'), marks = field.querySelectorAll('.st-marks > *');
  var value = field.querySelector('.st-value'), price = field.querySelector('.st-price'), per = field.querySelector('.st-per');
  function sync() {
    var i = range.value - range.min, text = labels[i];
    field.style.setProperty('--p', i / (range.max - range.min));
    stars.forEach(function (s, n) { s.toggleAttribute('data-on', n <= i); });
    marks.forEach(function (m, n) { m.toggleAttribute('data-on', n === i); });
    if (value) value.textContent = text;
    if (stars.length) text = (i + 1) + ' of ' + stars.length + ', ' + text;
    if (price) {
      price.textContent = '$' + Number(prices[i]).toLocaleString('en-US');
      per.textContent = 'per month · $' + prices[i] / seats[i] + ' a seat';
      text += ', $' + Number(prices[i]).toLocaleString('en-US') + ' per month';
    }
    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-[#5a5670] uppercase">Rating with stars</span><div class="rounded-2xl border border-[#e6e1f3] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(30,27,46,.04)]">
<div class="st grid gap-3 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="st-stay" class="text-[.95rem] font-semibold">How was your stay?</label><output for="st-stay" class="st-value text-[.95rem] font-bold text-st-accent">Great</output></div>
  <div class="st-stars mx-3 flex justify-between *:flex *:w-0 *:justify-center *:whitespace-nowrap" aria-hidden="true">
    <span><svg class="size-[26px] flex-none fill-[#c4b5fd] transition-[fill] duration-150 in-data-on:fill-[#f59e0b] motion-reduce:transition-none" viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg class="size-[26px] flex-none fill-[#c4b5fd] transition-[fill] duration-150 in-data-on:fill-[#f59e0b] motion-reduce:transition-none" viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg class="size-[26px] flex-none fill-[#c4b5fd] transition-[fill] duration-150 in-data-on:fill-[#f59e0b] motion-reduce:transition-none" viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg class="size-[26px] flex-none fill-[#c4b5fd] transition-[fill] duration-150 in-data-on:fill-[#f59e0b] motion-reduce:transition-none" viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
    <span><svg class="size-[26px] flex-none fill-[#c4b5fd] transition-[fill] duration-150 in-data-on:fill-[#f59e0b] motion-reduce:transition-none" viewBox="0 0 24 24"><path d="m12 2.8 2.8 5.8 6.3.9-4.6 4.4 1.1 6.3L12 17.2l-5.6 3 1.1-6.3L2.9 9.5l6.3-.9z"/></svg></span>
  </div>
  <input type="range" id="st-stay" min="1" max="5" step="1" value="4" data-labels="Poor|Fair|Good|Great|Superb" class="block h-[24px] w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-[6px] [&::-webkit-slider-runnable-track]:rounded-full [&::-moz-range-track]:h-[6px] [&::-moz-range-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--color-st-accent)_0_calc(12px_+_(100%_-_24px)_*_var(--p)),var(--color-st-track)_0)] [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-st-accent)_0_calc(12px_+_(100%_-_24px)_*_var(--p)),var(--color-st-track)_0)] [&::-webkit-slider-thumb]:-mt-[9px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-[24px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-3 [&::-webkit-slider-thumb]:border-st-accent [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(30,27,46,.3)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[24px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-3 [&::-moz-range-thumb]:border-st-accent [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(30,27,46,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-st-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-st-accent)]">
  <div class="st-marks mx-3 flex justify-between text-[.8rem] text-[#475569] *:flex *:w-0 *:justify-center *:whitespace-nowrap *:data-on:font-bold *:data-on:text-st-accent" aria-hidden="true"><span>Poor</span><span>Fair</span><span>Good</span><span>Great</span><span>Superb</span></div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5a5670] uppercase">Segmented size picker</span><div class="rounded-2xl border border-[#e6e1f3] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(30,27,46,.04)]">
<div class="st grid gap-3 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="st-size" class="text-[.95rem] font-semibold">T-shirt size</label><output for="st-size" class="st-value text-[.95rem] font-bold text-st-accent">Medium</output></div>
  <div class="relative">
    <input type="range" id="st-size" min="0" max="5" step="1" value="2" data-labels="Extra small|Small|Medium|Large|Extra large|Double extra large" class="block h-10 w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-10 [&::-webkit-slider-runnable-track]:rounded-xl [&::-webkit-slider-runnable-track]:bg-[#ede9fe] [&::-webkit-slider-runnable-track]:shadow-[inset_0_0_0_1px_#8b7fb0] [&::-moz-range-track]:h-10 [&::-moz-range-track]:rounded-xl [&::-moz-range-track]:bg-[#ede9fe] [&::-moz-range-track]:shadow-[inset_0_0_0_1px_#8b7fb0] [&::-webkit-slider-thumb]:mt-0 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:h-10 [&::-webkit-slider-thumb]:w-[52px] [&::-webkit-slider-thumb]:rounded-xl [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:bg-st-accent [&::-webkit-slider-thumb]:shadow-[0_2px_6px_rgba(109,40,217,.35)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:h-10 [&::-moz-range-thumb]:w-[52px] [&::-moz-range-thumb]:rounded-xl [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-st-accent [&::-moz-range-thumb]:shadow-[0_2px_6px_rgba(109,40,217,.35)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-st-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_2px_#fff,0_0_0_4px_var(--color-st-accent)]">
    <div class="st-marks pointer-events-none absolute inset-0 mx-[26px] flex items-center justify-between text-[.85rem] font-bold text-[#3b3355] *:flex *:w-0 *:justify-center *:whitespace-nowrap *:data-on:text-white" aria-hidden="true"><span>XS</span><span>S</span><span>M</span><span>L</span><span>XL</span><span>XXL</span></div>
  </div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5a5670] uppercase">Pricing by team size</span><div class="rounded-2xl border border-[#e6e1f3] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(30,27,46,.04)]">
<div class="st grid gap-3 [--p:0]">
  <div class="flex items-baseline justify-between gap-3"><label for="st-seats" class="text-[.95rem] font-semibold">Team size</label><output for="st-seats" class="st-value text-[.95rem] font-bold text-st-accent">25 seats</output></div>
  <div class="flex flex-wrap items-baseline gap-x-2.5 gap-y-1"><output for="st-seats" class="st-price text-[2.2rem] font-extrabold tracking-[-.02em] tabular-nums">$250</output><span class="st-per text-[.9rem] text-[#475569]">per month · $10 a seat</span></div>
  <input type="range" id="st-seats" min="0" max="5" step="1" value="2" data-labels="5 seats|10 seats|25 seats|50 seats|100 seats|250 seats" data-seats="5|10|25|50|100|250" data-prices="60|110|250|450|800|1750" class="block h-[24px] w-full cursor-pointer appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-[6px] [&::-webkit-slider-runnable-track]:rounded-full [&::-moz-range-track]:h-[6px] [&::-moz-range-track]:rounded-full [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--color-st-accent)_0_calc(12px_+_(100%_-_24px)_*_var(--p)),var(--color-st-track)_0)] [&::-moz-range-track]:bg-[linear-gradient(to_right,var(--color-st-accent)_0_calc(12px_+_(100%_-_24px)_*_var(--p)),var(--color-st-track)_0)] [&::-webkit-slider-thumb]:-mt-[9px] [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-[24px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-3 [&::-webkit-slider-thumb]:border-st-accent [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(30,27,46,.3)] [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[24px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-3 [&::-moz-range-thumb]:border-st-accent [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(30,27,46,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-st-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-st-accent)]">
  <div class="st-marks mx-3 flex justify-between text-[.8rem] text-[#475569] *:flex *:w-0 *:justify-center *:whitespace-nowrap *:data-on:font-bold *:data-on:text-st-accent" aria-hidden="true"><span>5</span><span>10</span><span>25</span><span>50</span><span>100</span><span>250</span></div>
</div></div></div>
CSS
@theme {
  --color-st-accent: #6d28d9;
  --color-st-track: #8b7fb0;
}
JavaScript
// Step sliders: maps each stop to a word, updates the readouts and aria-valuetext, and marks stars and labels.
document.querySelectorAll('.st input[type="range"]').forEach(function (range) {
  var field = range.closest('.st');
  var labels = range.dataset.labels.split('|');
  var seats = (range.dataset.seats || '').split('|'), prices = (range.dataset.prices || '').split('|');
  var stars = field.querySelectorAll('.st-stars > *'), marks = field.querySelectorAll('.st-marks > *');
  var value = field.querySelector('.st-value'), price = field.querySelector('.st-price'), per = field.querySelector('.st-per');
  function sync() {
    var i = range.value - range.min, text = labels[i];
    field.style.setProperty('--p', i / (range.max - range.min));
    stars.forEach(function (s, n) { s.toggleAttribute('data-on', n <= i); });
    marks.forEach(function (m, n) { m.toggleAttribute('data-on', n === i); });
    if (value) value.textContent = text;
    if (stars.length) text = (i + 1) + ' of ' + stars.length + ', ' + text;
    if (price) {
      price.textContent = '$' + Number(prices[i]).toLocaleString('en-US');
      per.textContent = 'per month · $' + prices[i] / seats[i] + ' a seat';
      text += ', $' + Number(prices[i]).toLocaleString('en-US') + ' per month';
    }
    range.setAttribute('aria-valuetext', text);
  }
  range.addEventListener('input', sync);
  sync();
});

About this set

Sliders whose positions stand for words or plans rather than raw numbers. Rating with stars asks how a stay was on a five-point scale, fills the stars up to the chosen point and prints Poor, Fair, Good, Great or Superb under the track, with the current word highlighted. Segmented size picker hides a range input inside a tall track with the sizes XS to XXL printed on it, and the thumb is a violet pill that slides under the chosen size, which then turns white. Pricing by team size is the pricing page pattern: six seat tiers from 5 to 250 on a slider, with a large monthly price and the price per seat recalculated as it moves. Each is a real range input with step=”1″ whose positions index a list in data-labels. The script writes the word into the output and into aria-valuetext, sets –p for the fill and marks the matching stars and labels with a data-on attribute, and for the pricing slider it reads data-seats and data-prices. Use it when a value has a name, when steps are uneven, or when a number alone would mean little.

What’s included

  • Word labels from a data-labels list
  • Stars that fill up to the chosen rating
  • Segmented size picker with the labels inside the track
  • Pricing slider with monthly and per-seat price
  • aria-valuetext such as 4 of 5, Great

Accessibility

Screen readers hear the word, not the index: the script sets aria-valuetext to values such as 4 of 5, Great or 25 seats, $250 per month. Stars and printed labels are aria-hidden because the input already exposes the value. Each slider has a visible label, works with the arrow keys and shows a focus ring on the thumb. The star colour change is the only transition and it is removed under prefers-reduced-motion.

Customise it

On .st change --st-accent, --st-track, --st-thumb and --st-h (plain CSS and Bootstrap) or the --color-st-* theme colours in Tailwind. Edit the words in data-labels, and the tiers in data-seats and data-prices, keeping min, max and the number of printed labels in step.