Open demo

CSS code

HTML
<div><span class="lbl">Seat slider with volume rates</span>
<section class="us us-seats" data-pricing="seats" aria-labelledby="hb-title">
  <div class="us-main">
    <p class="us-kicker">Huddleboard</p>
    <h2 class="us-title" id="hb-title">Pay for the seats you use</h2>
    <p class="us-desc">Every seat gets unlimited boards, video huddles and version history. The rate drops as your team grows.</p>
    <div class="us-field">
      <div class="us-field-top"><label for="hb-seats">Team size</label><span class="us-count" data-out="seats">12 seats</span></div>
      <input class="us-range" id="hb-seats" type="range" min="1" max="150" step="1" value="12" aria-valuetext="12 seats">
      <p class="us-ends" aria-hidden="true"><span>1</span><span>150</span></p>
    </div>
    <ul class="us-tiers" aria-label="Rates per seat">
        <li data-from="1" data-rate="8" data-on><b>$8 <span aria-hidden="true">/ seat</span><span class="sr-only">a seat</span></b>1–24 seats</li>
        <li data-from="25" data-rate="7"><b>$7 <span aria-hidden="true">/ seat</span><span class="sr-only">a seat</span></b>25–99 seats</li>
        <li data-from="100" data-rate="6"><b>$6 <span aria-hidden="true">/ seat</span><span class="sr-only">a seat</span></b>100+ seats</li>
    </ul>
  </div>
  <div class="us-side">
    <h3>Your price</h3>
    <p class="us-total"><output for="hb-seats" data-out="total">$96</output><span>per month</span></p>
    <p class="us-calc">Seats at <span data-out="rate">$8</span> each, billed monthly.</p>
    <a class="us-btn" href="#">Start a 14-day trial</a>
    <p class="us-small">Add or remove seats at any time.</p>
  </div>
</section></div>

<div><span class="lbl">Volume steps with a price per thousand</span>
<section class="us us-volume" data-pricing="volume" data-steps="[[10000,0],[50000,20],[100000,35],[250000,75],[500000,130],[1000000,230],[2000000,null]]" aria-labelledby="pw-title">
  <p class="us-kicker">Postwire email API</p>
  <h2 class="us-title" id="pw-title">Pricing that follows your sending</h2>
  <div class="us-vol-top">
    <div><label for="pw-volume">Emails a month</label><span class="us-vol" data-out="volume">100,000 emails a month</span></div>
    <p class="us-vol-price"><output for="pw-volume" data-out="total">$35</output><span data-out="unit">per month, $0.35 per 1,000 emails</span></p>
  </div>
  <input class="us-range" id="pw-volume" type="range" min="0" max="6" step="1" value="2" aria-valuetext="100,000 emails a month" style="--fill:33.3%">
  <ul class="us-ticks" aria-hidden="true"><li>10k</li><li>50k</li><li>100k</li><li>250k</li><li>500k</li><li>1M</li><li>2M+</li></ul>
  <div class="us-vol-foot">
    <ul class="us-list">
        <li><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="sr-only">Included: </span>Webhooks and open tracking</span></li>
        <li><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="sr-only">Included: </span>30 days of message logs</span></li>
        <li><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="sr-only">Included: </span>Dedicated IP from 250,000 emails</span></li>
    </ul>
    <a class="us-btn" href="#" data-out="cta">Start sending</a>
  </div>
</section></div>
CSS
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
/* Usage-based pricing: a range input recalculates the price with a few lines of JavaScript. */
.us{--us-accent:#6d28d9;--us-accent-dark:#5b21b6;--us-ink:#1e1b2e;--us-muted:#5b5670;--us-line:#e4e0f5;--us-track:#948abf;--us-tint:#f1edff;--us-radius:22px;color:var(--us-ink)}
.us *,.us *::before,.us *::after{box-sizing:border-box}
:where(.us) h2,:where(.us) p{margin:0}

/* the range input: filled track via --fill, set by the script */
.us-range{--fill:7.4%;display:block;width:100%;height:30px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:pointer}
.us-range::-webkit-slider-runnable-track{height:8px;border-radius:999px;background:linear-gradient(to right,var(--us-accent) var(--fill),var(--us-track) var(--fill))}
.us-range::-moz-range-track{height:8px;border-radius:999px;background:var(--us-track)}
.us-range::-moz-range-progress{height:8px;border-radius:999px;background:var(--us-accent)}
.us-range::-webkit-slider-thumb{width:26px;height:26px;margin-top:-9px;border:4px solid #fff;border-radius:50%;background:var(--us-accent);box-shadow:0 0 0 1px var(--us-accent),0 2px 8px rgba(30,27,46,.3);-webkit-appearance:none;appearance:none}
.us-range::-moz-range-thumb{width:18px;height:18px;border:4px solid #fff;border-radius:50%;background:var(--us-accent);box-shadow:0 0 0 1px var(--us-accent),0 2px 8px rgba(30,27,46,.3)}
.us-range:focus{outline:none}
.us-range:focus-visible{outline:3px solid var(--us-accent);outline-offset:6px;border-radius:999px}

.us-kicker{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--us-accent)}
.us-title{margin-top:8px;font-size:clamp(1.45rem,3.4vw,1.85rem);font-weight:800;line-height:1.2;letter-spacing:-.02em}
.us-desc{margin-top:10px;font-size:.95rem;line-height:1.55;color:var(--us-muted)}
.us-btn{display:block;margin-top:22px;padding:14px 20px;border-radius:12px;background:var(--us-accent);color:#fff;font-size:1rem;font-weight:700;line-height:1.3;text-align:center;text-decoration:none;transition:background-color .2s}
.us-btn:hover{background:var(--us-accent-dark)}
.us-btn:focus-visible{outline:3px solid var(--us-accent);outline-offset:3px}
.us-small{margin-top:12px;font-size:.84rem;line-height:1.45;color:var(--us-muted)}
/* 1. seats */
.us-seats{display:grid;grid-template-columns:1fr;background:#fff;border:1px solid var(--us-line);border-radius:var(--us-radius);overflow:hidden}
@media (min-width:768px){.us-seats{grid-template-columns:minmax(0,3fr) minmax(0,2fr)}}
.us-main{padding:34px 32px}
.us-field{margin-top:28px}
.us-field-top{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:12px}
.us-field label{font-size:.9rem;font-weight:700}
.us-count{font-size:1.15rem;font-weight:800;font-variant-numeric:tabular-nums}
.us-ends{display:flex;justify-content:space-between;margin-top:6px;font-size:.8rem;color:var(--us-muted)}
.us-tiers{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:24px 0 0;padding:0;list-style:none}
.us-tiers li{display:flex;flex-direction:column;gap:2px;padding:10px 12px;border:1.5px solid var(--us-line);border-radius:12px;font-size:.8rem;color:var(--us-muted)}
.us-tiers b{font-size:1rem;color:var(--us-ink)}
.us-tiers b span{font-size:.75rem;font-weight:600;color:var(--us-muted)}
.us-tiers li[data-on]{border-color:var(--us-accent);background:var(--us-tint);color:var(--us-ink)}
.us-side{display:flex;flex-direction:column;justify-content:center;padding:34px 32px;background:var(--us-tint)}
.us-side h3{margin:0;font-size:.9rem;font-weight:700}
.us-total{display:flex;align-items:baseline;flex-wrap:wrap;gap:4px 8px;margin-top:10px}
.us-total output{font-size:3.2rem;font-weight:800;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums}
.us-total span{font-size:.95rem;color:var(--us-muted)}
.us-calc{margin-top:8px;font-size:.9rem;color:var(--us-muted)}
/* 2. volume */
.us-volume{padding:34px 32px 30px;background:#fff;border:1px solid var(--us-line);border-radius:var(--us-radius)}
.us-vol-top{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:18px 32px;margin:24px 0 22px}
.us-vol-top label{display:block;font-size:.9rem;font-weight:700}
.us-vol{display:block;margin-top:6px;font-size:clamp(1.3rem,3vw,1.6rem);font-weight:800;font-variant-numeric:tabular-nums}
.us-vol-price{text-align:right}
.us-vol-price output{display:block;font-size:2.6rem;font-weight:800;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums}
.us-vol-price span{display:block;margin-top:6px;font-size:.86rem;color:var(--us-muted)}
.us-ticks{display:flex;justify-content:space-between;margin:8px 0 0;padding:0 2px;list-style:none;font-size:.76rem;font-weight:600;color:var(--us-muted)}
.us-vol-foot{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px 28px;margin-top:26px;padding-top:24px;border-top:1px solid var(--us-line)}
.us-list{display:flex;flex-wrap:wrap;gap:8px 22px;margin:0;padding:0;list-style:none;font-size:.9rem;line-height:1.4}
.us-list li{display:flex;align-items:flex-start;gap:8px}
.us-list svg{flex:none;margin-top:1px;color:var(--us-accent)}
.us-vol-foot .us-btn{margin-top:0;padding-inline:26px}
@media (max-width:575.98px){.us-vol-price{text-align:left}.us-vol-foot .us-btn{width:100%}.us-ticks li:nth-child(even){visibility:hidden}}
@media (prefers-reduced-motion:reduce){.us-btn{transition:none}}
JavaScript
/* Usage pricing: recalculate the price as the range input moves and keep aria-valuetext in step with it.
   Seats: the rate comes from the [data-from][data-rate] tier list. Volume: the steps live in data-steps. */
document.querySelectorAll('[data-pricing]').forEach(function (root) {
  var range = root.querySelector('input[type=range]');
  var out = function (name) { return root.querySelector('[data-out="' + name + '"]'); };
  var fmt = function (n) { return n.toLocaleString('en-US'); };
  var update;
  if (root.dataset.pricing === 'seats') {
    var tiers = Array.prototype.slice.call(root.querySelectorAll('[data-rate]'));
    update = function () {
      var n = +range.value;
      var tier = tiers.filter(function (t) { return n >= +t.dataset.from; }).pop();
      var rate = +tier.dataset.rate, seats = n === 1 ? '1 seat' : n + ' seats';
      range.setAttribute('aria-valuetext', seats);
      out('seats').textContent = seats;
      out('rate').textContent = '$' + rate;
      out('total').textContent = '$' + fmt(n * rate);
      tiers.forEach(function (t) { t.toggleAttribute('data-on', t === tier); });
    };
  } else {
    var steps = JSON.parse(root.dataset.steps);
    update = function () {
      var s = steps[+range.value], custom = s[1] === null;
      var volume = fmt(s[0]) + (custom ? '+' : '') + ' emails a month';
      range.setAttribute('aria-valuetext', volume);
      out('volume').textContent = volume;
      out('total').textContent = custom ? 'Custom' : '$' + fmt(s[1]);
      out('unit').textContent = custom ? 'Volume pricing, billed yearly' : s[1] === 0 ? 'Free, no card needed' : 'per month, $' + (s[1] / s[0] * 1000).toFixed(2) + ' per 1,000 emails';
      out('cta').textContent = custom ? 'Talk to sales' : 'Start sending';
    };
  }
  var paint = function () { range.style.setProperty('--fill', (range.value - range.min) / (range.max - range.min) * 100 + '%'); };
  range.addEventListener('input', function () { update(); paint(); });
  update(); paint();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Seat slider with volume rates</span>
<section class="us card us-card us-seats" data-pricing="seats" aria-labelledby="hb-title">
  <div class="row">
  <div class="col-12 col-md-7 us-main">
    <p class="us-kicker">Huddleboard</p>
    <h2 class="us-title" id="hb-title">Pay for the seats you use</h2>
    <p class="us-desc">Every seat gets unlimited boards, video huddles and version history. The rate drops as your team grows.</p>
    <div class="us-field">
      <div class="us-field-top d-flex align-items-baseline justify-content-between"><label class="form-label" for="hb-seats">Team size</label><span class="us-count" data-out="seats">12 seats</span></div>
      <input class="form-range us-range" id="hb-seats" type="range" min="1" max="150" step="1" value="12" aria-valuetext="12 seats">
      <p class="us-ends d-flex justify-content-between" aria-hidden="true"><span>1</span><span>150</span></p>
    </div>
    <ul class="list-group us-tiers" aria-label="Rates per seat">
        <li class="list-group-item" data-from="1" data-rate="8" data-on><b>$8 <span aria-hidden="true">/ seat</span><span class="visually-hidden">a seat</span></b>1–24 seats</li>
        <li class="list-group-item" data-from="25" data-rate="7"><b>$7 <span aria-hidden="true">/ seat</span><span class="visually-hidden">a seat</span></b>25–99 seats</li>
        <li class="list-group-item" data-from="100" data-rate="6"><b>$6 <span aria-hidden="true">/ seat</span><span class="visually-hidden">a seat</span></b>100+ seats</li>
    </ul>
  </div>
  <div class="col-12 col-md-5 us-side d-flex flex-column justify-content-center">
    <h3>Your price</h3>
    <p class="us-total d-flex flex-wrap align-items-baseline"><output for="hb-seats" data-out="total">$96</output><span>per month</span></p>
    <p class="us-calc">Seats at <span data-out="rate">$8</span> each, billed monthly.</p>
    <a class="btn us-btn d-block" href="#">Start a 14-day trial</a>
    <p class="us-small">Add or remove seats at any time.</p>
  </div>
  </div>
</section></div>

<div><span class="lbl">Volume steps with a price per thousand</span>
<section class="us card us-card us-volume" data-pricing="volume" data-steps="[[10000,0],[50000,20],[100000,35],[250000,75],[500000,130],[1000000,230],[2000000,null]]" aria-labelledby="pw-title"><div class="card-body">
  <p class="us-kicker">Postwire email API</p>
  <h2 class="us-title" id="pw-title">Pricing that follows your sending</h2>
  <div class="us-vol-top d-flex flex-wrap align-items-end justify-content-between">
    <div><label class="form-label" for="pw-volume">Emails a month</label><span class="us-vol" data-out="volume">100,000 emails a month</span></div>
    <p class="us-vol-price text-end"><output for="pw-volume" data-out="total">$35</output><span data-out="unit">per month, $0.35 per 1,000 emails</span></p>
  </div>
  <input class="form-range us-range" id="pw-volume" type="range" min="0" max="6" step="1" value="2" aria-valuetext="100,000 emails a month" style="--fill:33.3%">
  <ul class="us-ticks list-unstyled d-flex justify-content-between" aria-hidden="true"><li>10k</li><li>50k</li><li>100k</li><li>250k</li><li>500k</li><li>1M</li><li>2M+</li></ul>
  <div class="us-vol-foot d-flex flex-wrap align-items-center justify-content-between">
    <ul class="us-list list-unstyled d-flex flex-wrap">
        <li><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="visually-hidden">Included: </span>Webhooks and open tracking</span></li>
        <li><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="visually-hidden">Included: </span>30 days of message logs</span></li>
        <li><svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="visually-hidden">Included: </span>Dedicated IP from 250,000 emails</span></li>
    </ul>
    <a class="btn us-btn" href="#" data-out="cta">Start sending</a>
  </div>
</div></section></div>
CSS
/* Usage-based pricing on Bootstrap's .form-range, .card, .list-group and .btn; a few lines of JavaScript recalculate the price. */
.us{--us-accent:#6d28d9;--us-accent-dark:#5b21b6;--us-ink:#1e1b2e;--us-muted:#5b5670;--us-line:#e4e0f5;--us-track:#948abf;--us-tint:#f1edff;--us-radius:22px;color:var(--us-ink)}
:where(.us) h2,:where(.us) p{margin:0}

/* the range input: filled track via --fill, set by the script */
.form-range.us-range{--fill:7.4%;display:block;width:100%;height:30px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:pointer}
.us-range::-webkit-slider-runnable-track{height:8px;border-radius:999px;background:linear-gradient(to right,var(--us-accent) var(--fill),var(--us-track) var(--fill))}
.us-range::-moz-range-track{height:8px;border-radius:999px;background:var(--us-track)}
.us-range::-moz-range-progress{height:8px;border-radius:999px;background:var(--us-accent)}
.us-range::-webkit-slider-thumb{width:26px;height:26px;margin-top:-9px;border:4px solid #fff;border-radius:50%;background:var(--us-accent);box-shadow:0 0 0 1px var(--us-accent),0 2px 8px rgba(30,27,46,.3);-webkit-appearance:none;appearance:none}
.us-range::-moz-range-thumb{width:18px;height:18px;border:4px solid #fff;border-radius:50%;background:var(--us-accent);box-shadow:0 0 0 1px var(--us-accent),0 2px 8px rgba(30,27,46,.3)}
.form-range.us-range:focus{outline:none}
.form-range.us-range:focus-visible{outline:3px solid var(--us-accent);outline-offset:6px;border-radius:999px}

.us-card{--bs-card-border-color:var(--us-line);--bs-card-border-radius:22px;--bs-card-inner-border-radius:21px;--bs-card-color:var(--us-ink);overflow:hidden}
.us-kicker{font-size:.78rem;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--us-accent)}
.us .us-title{margin-top:8px;font-size:clamp(1.45rem,3.4vw,1.85rem);font-weight:800;line-height:1.2;letter-spacing:-.02em}
.us .us-desc{margin-top:10px;font-size:.95rem;line-height:1.55;color:var(--us-muted)}
.us-btn{--bs-btn-padding-y:14px;--bs-btn-padding-x:20px;--bs-btn-font-size:1rem;--bs-btn-font-weight:700;--bs-btn-line-height:1.3;--bs-btn-border-width:0;--bs-btn-border-radius:12px;
  --bs-btn-color:#fff;--bs-btn-bg:var(--us-accent);--bs-btn-hover-color:#fff;--bs-btn-hover-bg:var(--us-accent-dark);--bs-btn-active-color:#fff;--bs-btn-active-bg:var(--us-accent-dark);margin-top:22px;transition:background-color .2s}
.us-btn:focus-visible{outline:3px solid var(--us-accent);outline-offset:3px;box-shadow:none}
.us .us-small{margin-top:12px;font-size:.84rem;line-height:1.45;color:var(--us-muted)}
.us-seats .row{--bs-gutter-x:0}
.us-main{padding:34px 32px}
.us-field{margin-top:28px}
.us-field-top{gap:12px;margin-bottom:12px}
.us-field .form-label{margin:0;font-size:.9rem;font-weight:700}
.us-count{font-size:1.15rem;font-weight:800;font-variant-numeric:tabular-nums}
.us .us-ends{margin-top:6px;font-size:.8rem;color:var(--us-muted)}
.us-tiers{--bs-list-group-border-color:var(--us-line);--bs-list-group-border-width:1.5px;--bs-list-group-border-radius:12px;--bs-list-group-item-padding-x:12px;--bs-list-group-item-padding-y:10px;--bs-list-group-color:var(--us-muted);
  display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:24px}
.us-tiers .list-group-item{display:flex;flex-direction:column;gap:2px;border-width:1.5px;border-radius:12px;font-size:.8rem}
.us-tiers .list-group-item+.list-group-item{border-top-width:1.5px}
.us-tiers b{font-size:1rem;color:var(--us-ink)}
.us-tiers b span{font-size:.75rem;font-weight:600;color:var(--us-muted)}
.us-tiers .list-group-item[data-on]{border-color:var(--us-accent);background:var(--us-tint);color:var(--us-ink)}
.us-side{padding:34px 32px;background:var(--us-tint)}
.us-side h3{margin:0;font-size:.9rem;font-weight:700}
.us .us-total{gap:4px 8px;margin-top:10px}
.us-total output{font-size:3.2rem;font-weight:800;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums}
.us-total span{font-size:.95rem;color:var(--us-muted)}
.us .us-calc{margin-top:8px;font-size:.9rem;color:var(--us-muted)}
.us-volume{--bs-card-spacer-y:34px;--bs-card-spacer-x:32px}
.us-volume .card-body{padding-bottom:30px}
.us-vol-top{gap:18px 32px;margin:24px 0 22px}
.us-vol-top .form-label{display:block;margin:0;font-size:.9rem;font-weight:700}
.us-vol{display:block;margin-top:6px;font-size:clamp(1.3rem,3vw,1.6rem);font-weight:800;font-variant-numeric:tabular-nums}
.us-vol-price output{display:block;font-size:2.6rem;font-weight:800;line-height:1;letter-spacing:-.04em;font-variant-numeric:tabular-nums}
.us-vol-price span{display:block;margin-top:6px;font-size:.86rem;color:var(--us-muted)}
.us-ticks{margin:8px 0 0;padding:0 2px;font-size:.76rem;font-weight:600;color:var(--us-muted)}
.us-vol-foot{gap:16px 28px;margin-top:26px;padding-top:24px;border-top:1px solid var(--us-line)}
.us-list{gap:8px 22px;margin:0;font-size:.9rem;line-height:1.4}
.us-list li{display:flex;align-items:flex-start;gap:8px}
.us-list svg{flex:none;margin-top:1px;color:var(--us-accent)}
.us-vol-foot .us-btn{margin-top:0;--bs-btn-padding-x:26px}
@media (max-width:575.98px){.us-vol-price{text-align:left!important}.us-vol-foot .us-btn{width:100%}.us-ticks li:nth-child(even){visibility:hidden}}
@media (prefers-reduced-motion:reduce){.us-btn{transition:none}}
JavaScript
/* Usage pricing: recalculate the price as the range input moves and keep aria-valuetext in step with it.
   Seats: the rate comes from the [data-from][data-rate] tier list. Volume: the steps live in data-steps. */
document.querySelectorAll('[data-pricing]').forEach(function (root) {
  var range = root.querySelector('input[type=range]');
  var out = function (name) { return root.querySelector('[data-out="' + name + '"]'); };
  var fmt = function (n) { return n.toLocaleString('en-US'); };
  var update;
  if (root.dataset.pricing === 'seats') {
    var tiers = Array.prototype.slice.call(root.querySelectorAll('[data-rate]'));
    update = function () {
      var n = +range.value;
      var tier = tiers.filter(function (t) { return n >= +t.dataset.from; }).pop();
      var rate = +tier.dataset.rate, seats = n === 1 ? '1 seat' : n + ' seats';
      range.setAttribute('aria-valuetext', seats);
      out('seats').textContent = seats;
      out('rate').textContent = '$' + rate;
      out('total').textContent = '$' + fmt(n * rate);
      tiers.forEach(function (t) { t.toggleAttribute('data-on', t === tier); });
    };
  } else {
    var steps = JSON.parse(root.dataset.steps);
    update = function () {
      var s = steps[+range.value], custom = s[1] === null;
      var volume = fmt(s[0]) + (custom ? '+' : '') + ' emails a month';
      range.setAttribute('aria-valuetext', volume);
      out('volume').textContent = volume;
      out('total').textContent = custom ? 'Custom' : '$' + fmt(s[1]);
      out('unit').textContent = custom ? 'Volume pricing, billed yearly' : s[1] === 0 ? 'Free, no card needed' : 'per month, $' + (s[1] / s[0] * 1000).toFixed(2) + ' per 1,000 emails';
      out('cta').textContent = custom ? 'Talk to sales' : 'Start sending';
    };
  }
  var paint = function () { range.style.setProperty('--fill', (range.value - range.min) / (range.max - range.min) * 100 + '%'); };
  range.addEventListener('input', function () { update(); paint(); });
  update(); paint();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5b5670] uppercase">Seat slider with volume rates</span>
<section class="grid grid-cols-1 overflow-hidden rounded-[22px] border border-[#e4e0f5] bg-white text-[#1e1b2e] md:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]" data-pricing="seats" aria-labelledby="hb-title">
  <div class="px-8 py-[34px]">
    <p class="text-[.78rem] font-bold tracking-[.14em] text-[#6d28d9] uppercase">Huddleboard</p>
    <h2 class="mt-2 text-[clamp(1.45rem,3.4vw,1.85rem)] leading-[1.2] font-extrabold tracking-[-.02em]" id="hb-title">Pay for the seats you use</h2>
    <p class="mt-2.5 text-[.95rem] leading-[1.55] text-[#5b5670]">Every seat gets unlimited boards, video huddles and version history. The rate drops as your team grows.</p>
    <div class="mt-7">
      <div class="mb-3 flex items-baseline justify-between gap-3"><label class="text-[.9rem] font-bold" for="hb-seats">Team size</label><span class="text-[1.15rem] font-extrabold tabular-nums" data-out="seats">12 seats</span></div>
      <input class="us-range" id="hb-seats" type="range" min="1" max="150" step="1" value="12" aria-valuetext="12 seats">
      <p class="mt-1.5 flex justify-between text-[.8rem] text-[#5b5670]" aria-hidden="true"><span>1</span><span>150</span></p>
    </div>
    <ul class="mt-6 grid grid-cols-3 gap-2" aria-label="Rates per seat">
        <li class="flex flex-col gap-0.5 rounded-xl border-[1.5px] border-[#e4e0f5] px-3 py-2.5 text-[.8rem] text-[#5b5670] data-[on]:border-[#6d28d9] data-[on]:bg-[#f1edff] data-[on]:text-[#1e1b2e]" data-from="1" data-rate="8" data-on><b class="text-[1rem] text-[#1e1b2e]">$8 <span class="text-[.75rem] font-semibold text-[#5b5670]" aria-hidden="true">/ seat</span><span class="sr-only">a seat</span></b>1–24 seats</li>
        <li class="flex flex-col gap-0.5 rounded-xl border-[1.5px] border-[#e4e0f5] px-3 py-2.5 text-[.8rem] text-[#5b5670] data-[on]:border-[#6d28d9] data-[on]:bg-[#f1edff] data-[on]:text-[#1e1b2e]" data-from="25" data-rate="7"><b class="text-[1rem] text-[#1e1b2e]">$7 <span class="text-[.75rem] font-semibold text-[#5b5670]" aria-hidden="true">/ seat</span><span class="sr-only">a seat</span></b>25–99 seats</li>
        <li class="flex flex-col gap-0.5 rounded-xl border-[1.5px] border-[#e4e0f5] px-3 py-2.5 text-[.8rem] text-[#5b5670] data-[on]:border-[#6d28d9] data-[on]:bg-[#f1edff] data-[on]:text-[#1e1b2e]" data-from="100" data-rate="6"><b class="text-[1rem] text-[#1e1b2e]">$6 <span class="text-[.75rem] font-semibold text-[#5b5670]" aria-hidden="true">/ seat</span><span class="sr-only">a seat</span></b>100+ seats</li>
    </ul>
  </div>
  <div class="flex flex-col justify-center bg-[#f1edff] px-8 py-[34px]">
    <h3 class="text-[.9rem] font-bold">Your price</h3>
    <p class="mt-2.5 flex flex-wrap items-baseline gap-x-2 gap-y-1"><output class="text-[3.2rem] leading-none font-extrabold tracking-[-.04em] tabular-nums" for="hb-seats" data-out="total">$96</output><span class="text-[.95rem] text-[#5b5670]">per month</span></p>
    <p class="mt-2 text-[.9rem] text-[#5b5670]">Seats at <span data-out="rate">$8</span> each, billed monthly.</p>
    <a class="mt-[22px] block rounded-xl bg-[#6d28d9] px-5 py-3.5 text-center text-base leading-[1.3] font-bold text-white no-underline transition-[color,background-color,border-color] duration-200 hover:bg-[#5b21b6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#6d28d9] motion-reduce:transition-none" href="#">Start a 14-day trial</a>
    <p class="mt-3 text-[.84rem] leading-[1.45] text-[#5b5670]">Add or remove seats at any time.</p>
  </div>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5b5670] uppercase">Volume steps with a price per thousand</span>
<section class="rounded-[22px] border border-[#e4e0f5] bg-white px-8 pt-[34px] pb-[30px] text-[#1e1b2e]" data-pricing="volume" data-steps="[[10000,0],[50000,20],[100000,35],[250000,75],[500000,130],[1000000,230],[2000000,null]]" aria-labelledby="pw-title">
  <p class="text-[.78rem] font-bold tracking-[.14em] text-[#6d28d9] uppercase">Postwire email API</p>
  <h2 class="mt-2 text-[clamp(1.45rem,3.4vw,1.85rem)] leading-[1.2] font-extrabold tracking-[-.02em]" id="pw-title">Pricing that follows your sending</h2>
  <div class="mt-6 mb-[22px] flex flex-wrap items-end justify-between gap-x-8 gap-y-[18px]">
    <div><label class="block text-[.9rem] font-bold" for="pw-volume">Emails a month</label><span class="mt-1.5 block text-[clamp(1.3rem,3vw,1.6rem)] font-extrabold tabular-nums" data-out="volume">100,000 emails a month</span></div>
    <p class="text-right max-sm:text-left"><output class="block text-[2.6rem] leading-none font-extrabold tracking-[-.04em] tabular-nums" for="pw-volume" data-out="total">$35</output><span class="mt-1.5 block text-[.86rem] text-[#5b5670]" data-out="unit">per month, $0.35 per 1,000 emails</span></p>
  </div>
  <input class="us-range" id="pw-volume" type="range" min="0" max="6" step="1" value="2" aria-valuetext="100,000 emails a month" style="--fill:33.3%">
  <ul class="mt-2 flex justify-between px-0.5 text-[.76rem] font-semibold text-[#5b5670] max-sm:[&>li:nth-child(even)]:invisible" aria-hidden="true"><li>10k</li><li>50k</li><li>100k</li><li>250k</li><li>500k</li><li>1M</li><li>2M+</li></ul>
  <div class="mt-[26px] flex flex-wrap items-center justify-between gap-x-7 gap-y-4 border-t border-[#e4e0f5] pt-6">
    <ul class="flex flex-wrap gap-x-[22px] gap-y-2 text-[.9rem] leading-[1.4]">
        <li class="flex items-start gap-2"><svg class="mt-px flex-none text-[#6d28d9]" viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="sr-only">Included: </span>Webhooks and open tracking</span></li>
        <li class="flex items-start gap-2"><svg class="mt-px flex-none text-[#6d28d9]" viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="sr-only">Included: </span>30 days of message logs</span></li>
        <li class="flex items-start gap-2"><svg class="mt-px flex-none text-[#6d28d9]" viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><span><span class="sr-only">Included: </span>Dedicated IP from 250,000 emails</span></li>
    </ul>
    <a class="block rounded-xl bg-[#6d28d9] px-[26px] py-3.5 text-center text-base leading-[1.3] font-bold text-white no-underline transition-[color,background-color,border-color] duration-200 hover:bg-[#5b21b6] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-[#6d28d9] motion-reduce:transition-none max-sm:w-full" href="#" data-out="cta">Start sending</a>
  </div>
</section></div>
CSS
/* Range input track and thumb: no utility reaches these pseudo-elements cleanly. */
.us-range { --us-accent:#6d28d9;--us-accent-dark:#5b21b6;--us-ink:#1e1b2e;--us-muted:#5b5670;--us-line:#e4e0f5;--us-track:#948abf;--us-tint:#f1edff;--us-radius:22px; }

/* the range input: filled track via --fill, set by the script */
.us-range{--fill:7.4%;display:block;width:100%;height:30px;margin:0;padding:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:pointer}
.us-range::-webkit-slider-runnable-track{height:8px;border-radius:999px;background:linear-gradient(to right,var(--us-accent) var(--fill),var(--us-track) var(--fill))}
.us-range::-moz-range-track{height:8px;border-radius:999px;background:var(--us-track)}
.us-range::-moz-range-progress{height:8px;border-radius:999px;background:var(--us-accent)}
.us-range::-webkit-slider-thumb{width:26px;height:26px;margin-top:-9px;border:4px solid #fff;border-radius:50%;background:var(--us-accent);box-shadow:0 0 0 1px var(--us-accent),0 2px 8px rgba(30,27,46,.3);-webkit-appearance:none;appearance:none}
.us-range::-moz-range-thumb{width:18px;height:18px;border:4px solid #fff;border-radius:50%;background:var(--us-accent);box-shadow:0 0 0 1px var(--us-accent),0 2px 8px rgba(30,27,46,.3)}
.us-range:focus{outline:none}
.us-range:focus-visible{outline:3px solid var(--us-accent);outline-offset:6px;border-radius:999px}
JavaScript
/* Usage pricing: recalculate the price as the range input moves and keep aria-valuetext in step with it.
   Seats: the rate comes from the [data-from][data-rate] tier list. Volume: the steps live in data-steps. */
document.querySelectorAll('[data-pricing]').forEach(function (root) {
  var range = root.querySelector('input[type=range]');
  var out = function (name) { return root.querySelector('[data-out="' + name + '"]'); };
  var fmt = function (n) { return n.toLocaleString('en-US'); };
  var update;
  if (root.dataset.pricing === 'seats') {
    var tiers = Array.prototype.slice.call(root.querySelectorAll('[data-rate]'));
    update = function () {
      var n = +range.value;
      var tier = tiers.filter(function (t) { return n >= +t.dataset.from; }).pop();
      var rate = +tier.dataset.rate, seats = n === 1 ? '1 seat' : n + ' seats';
      range.setAttribute('aria-valuetext', seats);
      out('seats').textContent = seats;
      out('rate').textContent = '$' + rate;
      out('total').textContent = '$' + fmt(n * rate);
      tiers.forEach(function (t) { t.toggleAttribute('data-on', t === tier); });
    };
  } else {
    var steps = JSON.parse(root.dataset.steps);
    update = function () {
      var s = steps[+range.value], custom = s[1] === null;
      var volume = fmt(s[0]) + (custom ? '+' : '') + ' emails a month';
      range.setAttribute('aria-valuetext', volume);
      out('volume').textContent = volume;
      out('total').textContent = custom ? 'Custom' : '$' + fmt(s[1]);
      out('unit').textContent = custom ? 'Volume pricing, billed yearly' : s[1] === 0 ? 'Free, no card needed' : 'per month, $' + (s[1] / s[0] * 1000).toFixed(2) + ' per 1,000 emails';
      out('cta').textContent = custom ? 'Talk to sales' : 'Start sending';
    };
  }
  var paint = function () { range.style.setProperty('--fill', (range.value - range.min) / (range.max - range.min) * 100 + '%'); };
  range.addEventListener('input', function () { update(); paint(); });
  update(); paint();
});

About this set

Pricing calculators for products billed by seats or by volume, where a range slider recalculates the price as it moves. Seat slider with volume rates is a team whiteboard priced per seat, from 1 to 150 seats, with three rate bands of $8, $7 and $6 a seat; the band that applies is highlighted, and the total, the seat count and the rate in the sentence under it all update together. Volume steps with a price per thousand is an email API with seven sending volumes from 10,000 to over 2 million emails a month; each step shows the monthly price and the price per thousand emails, the first step is free and the last switches to custom pricing with a Talk to sales button. The slider is a native range input with a visible label and an aria-valuetext that reads 12 seats or 100,000 emails a month instead of a bare number, and the total is an output element so the new price is announced. About forty lines of vanilla JavaScript do the maths, and the page is rendered with the default values so it still reads correctly without the script.

What’s included

  • Per-seat pricing with volume rate bands and a per-volume calculator
  • Native range input with a visible label and a meaningful aria-valuetext
  • Total price in an output element, so changes are announced
  • About forty lines of vanilla JavaScript, no library
  • Bootstrap version uses .form-range, .list-group, .card and .btn

Accessibility

Each slider is a native range input with a visible label, and the script keeps aria-valuetext in step so assistive technology hears 12 seats or 100,000 emails a month. The arrow keys move the slider one step, the total is an output element tied to the slider, and the slider shows a 3px outline on keyboard focus. Button transitions stop under prefers-reduced-motion.

Customise it

Change --us-accent, --us-track and --us-tint on .us. Edit the data-from and data-rate attributes on the rate list for your own seat bands, and the data-steps array for volume steps; in Bootstrap the same variables restyle .form-range.