CSS + JSMIT licensed

Interactive Volume Slider Pricing

The usage-based pricing calculator SaaS teams actually ship: drag a "team size" range slider and every tier's monthly total recalculates live, the per-seat maths updates, and the recommended plan badge jumps to whichever tier is now the best deal.

Live Demo
Plain CSSTailwind v4
Try it

Complete Tailwind CSS landing page templates in HTML, React, Next.js and Vue · from $49

Cruip · Affiliate link

Browse Cruip templates

The code

<section class="prc-11" aria-label="Usage-based pricing calculator">
  <header class="prc-11__head">
    <h2>How big is your team?</h2>
    <div class="prc-11__sliderrow">
      <label for="prc-11-range" class="prc-11__count"><output id="prc-11-out">10</output> seats</label>
      <input type="range" id="prc-11-range" min="1" max="100" step="1" value="10" aria-describedby="prc-11-out">
    </div>
  </header>
  <div class="prc-11__grid" id="prc-11-grid">
    <article class="prc-11__card" data-tier="0"><span class="prc-11__best-badge">Best value</span><h3>Starter</h3><p class="prc-11__total"><span class="prc-11__amount">$70</span><small>/mo</small></p><p class="prc-11__math"></p><ul><li>Core features</li><li>Standard support</li></ul><a href="#" class="prc-11__cta">Choose Starter</a></article>
    <article class="prc-11__card" data-tier="1"><span class="prc-11__best-badge">Best value</span><h3>Team</h3><p class="prc-11__total"><span class="prc-11__amount">$99</span><small>/mo</small></p><p class="prc-11__math"></p><ul><li>Everything in Starter</li><li>Roles &amp; permissions</li><li>Priority support</li></ul><a href="#" class="prc-11__cta">Choose Team</a></article>
    <article class="prc-11__card" data-tier="2"><span class="prc-11__best-badge">Best value</span><h3>Business</h3><p class="prc-11__total"><span class="prc-11__amount">$249</span><small>/mo</small></p><p class="prc-11__math"></p><ul><li>Everything in Team</li><li>SSO &amp; audit log</li><li>99.9% SLA</li></ul><a href="#" class="prc-11__cta">Choose Business</a></article>
  </div>
  <p class="prc-11__live" id="prc-11-live" aria-live="polite"></p>
</section>
.prc-11,
.prc-11 *,
.prc-11 *::before,
.prc-11 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.prc-11 {
  --accent: oklch(0.62 0.18 145);
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #101511;
  color: #e9efe9;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 56px 24px;
}

.prc-11__head {
  text-align: center;
  margin-bottom: 38px;
}

.prc-11__head h2 {
  font-size: clamp(24px,3.6vw,34px);
  font-weight: 800;
  letter-spacing: -.02em;
  margin-bottom: 22px;
}

.prc-11__sliderrow {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: center;
}

.prc-11__count {
  font-size: 14px;
  font-weight: 600;
  color: #9fb0a0;
}

.prc-11__count output {
  font-size: 20px;
  font-weight: 800;
  color: var(--accent);
  margin-right: 4px;
}

.prc-11 input[type=range] {
  --pct: 9%;
  width: min(420px,86vw);
  height: 6px;
  border-radius: 99px;
  appearance: none;
  -webkit-appearance: none;
  background: linear-gradient(to right,var(--accent) var(--pct),#28322a var(--pct));
  cursor: pointer;
}

.prc-11 input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #fff;
  border: 5px solid var(--accent);
  box-shadow: 0 4px 12px rgba(0,0,0,.4);
  transition: transform .15s;
}

.prc-11 input[type=range]::-webkit-slider-thumb:hover {
  transform: scale(1.15);
}

.prc-11 input[type=range]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  border: 5px solid var(--accent);
}

.prc-11 input[type=range]:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 6px;
}

.prc-11__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(220px,1fr));
  gap: 18px;
  width: min(880px,100%);
  margin: 0 auto;
}

.prc-11__card {
  position: relative;
  background: #161d17;
  border: 1.5px solid #263028;
  border-radius: 18px;
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: border-color .35s,box-shadow .35s;
}

.prc-11__card.prc-11__best {
  border-color: var(--accent);
  box-shadow: 0 18px 44px -26px oklch(0.62 0.18 145/.6);
}

.prc-11__best-badge {
  position: absolute;
  top: -11px;
  right: 20px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 99px;
  background: var(--accent);
  color: #0c130d;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .35s,transform .35s;
}

.prc-11__card.prc-11__best .prc-11__best-badge {
  opacity: 1;
  transform: translateY(0);
}

.prc-11__card h3 {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #93a394;
}

.prc-11__total {
  font-size: 40px;
  font-weight: 800;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}

.prc-11__total small {
  font-size: 15px;
  font-weight: 600;
  color: #93a394;
}

.prc-11__math {
  font-size: 12px;
  color: #7c8c7d;
  min-height: 16px;
  margin-bottom: 10px;
  font-variant-numeric: tabular-nums;
}

.prc-11__card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 13.5px;
  color: #bccabc;
  flex: 1;
  margin-bottom: 18px;
}

.prc-11__card li::before {
  content: "✓";
  color: var(--accent);
  font-weight: 700;
  margin-right: 8px;
}

.prc-11__cta {
  text-align: center;
  text-decoration: none;
  font-weight: 700;
  font-size: 13.5px;
  padding: 12px;
  border-radius: 11px;
  border: 1px solid #2c382e;
  color: #e9efe9;
  transition: border-color .2s,background .2s;
}

.prc-11__cta:hover {
  border-color: var(--accent);
}

.prc-11__card.prc-11__best .prc-11__cta {
  background: var(--accent);
  border-color: var(--accent);
  color: #0c130d;
}

.prc-11__cta:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.prc-11__live {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .prc-11__card,
    .prc-11__best-badge {
    transition-duration: .01s;
  }
}
(() => {
  const root = document.querySelector('.prc-11');
  const range = root.querySelector('#prc-11-range');
  const out = root.querySelector('#prc-11-out');
  const live = root.querySelector('#prc-11-live');
  const cards = [...root.querySelectorAll('.prc-11__card')];
  const TIERS = [
    { name: 'Starter', base: 0, seat: 7 },
    { name: 'Team', base: 49, seat: 5 },
    { name: 'Business', base: 149, seat: 3.5 },
  ];
  const ANNOUNCE_MS = 600;
  let announceTimer;
  function update() {
    const seats = +range.value;
    out.textContent = seats;
    range.style.setProperty('--pct', ((seats - range.min) / (range.max - range.min) * 100) + '%');
    let bestIdx = 0, bestPrice = Infinity;
    TIERS.forEach((t, i) => {
      const total = Math.round(t.base + t.seat * seats);
      cards[i].querySelector('.prc-11__amount').textContent = '$' + total;
      cards[i].querySelector('.prc-11__math').textContent =
        (t.base ? '$' + t.base + ' base + ' : '') + '$' + t.seat + ' × ' + seats + ' seats';
      if (total < bestPrice) { bestPrice = total; bestIdx = i; }
    });
    cards.forEach((c, i) => c.classList.toggle('prc-11__best', i === bestIdx));
    clearTimeout(announceTimer);
    announceTimer = setTimeout(() => {
      live.textContent = seats + ' seats: best value is ' + TIERS[bestIdx].name + ' at $' + bestPrice + ' per month.';
    }, ANNOUNCE_MS);
  }
  range.addEventListener('input', update);
  update();
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Pricing Table from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Interactive Volume Slider Pricing
Source: https://codefronts.com/components/css-pricing-tables/interactive-volume-slider-pricing/

The usage-based pricing calculator SaaS teams actually ship: drag a "team size" range slider and every tier's monthly total recalculates live, the per-seat maths updates, and the recommended plan badge jumps to whichever tier is now the best deal.
## HTML
```html
<section class="prc-11" aria-label="Usage-based pricing calculator">
  <header class="prc-11__head">
    <h2>How big is your team?</h2>
    <div class="prc-11__sliderrow">
      <label for="prc-11-range" class="prc-11__count"><output id="prc-11-out">10</output> seats</label>
      <input type="range" id="prc-11-range" min="1" max="100" step="1" value="10" aria-describedby="prc-11-out">
    </div>
  </header>
  <div class="prc-11__grid" id="prc-11-grid">
    <article class="prc-11__card" data-tier="0"><span class="prc-11__best-badge">Best value</span><h3>Starter</h3><p class="prc-11__total"><span class="prc-11__amount">$70</span><small>/mo</small></p><p class="prc-11__math"></p><ul><li>Core features</li><li>Standard support</li></ul><a href="#" class="prc-11__cta">Choose Starter</a></article>
    <article class="prc-11__card" data-tier="1"><span class="prc-11__best-badge">Best value</span><h3>Team</h3><p class="prc-11__total"><span class="prc-11__amount">$99</span><small>/mo</small></p><p class="prc-11__math"></p><ul><li>Everything in Starter</li><li>Roles &amp; permissions</li><li>Priority support</li></ul><a href="#" class="prc-11__cta">Choose Team</a></article>
    <article class="prc-11__card" data-tier="2"><span class="prc-11__best-badge">Best value</span><h3>Business</h3><p class="prc-11__total"><span class="prc-11__amount">$249</span><small>/mo</small></p><p class="prc-11__math"></p><ul><li>Everything in Team</li><li>SSO &amp; audit log</li><li>99.9% SLA</li></ul><a href="#" class="prc-11__cta">Choose Business</a></article>
  </div>
  <p class="prc-11__live" id="prc-11-live" aria-live="polite"></p>
</section>
```
## CSS
```css
.prc-11,
.prc-11 *,
.prc-11 *::before,
.prc-11 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.prc-11 {
  --accent: oklch(0.62 0.18 145);
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: #101511;
  color: #e9efe9;
  width: 100%;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 56px 24px;
}

.prc-11__head {
  text-align: center;
  margin-bottom: 38px;
}

.prc-11__head h2 {
  font-size: clamp(24px,3.6vw,34px);
  font-weight: 800;
  letter-spacing: -.02em;
  margin-bottom: 22px;
}

.prc-11__sliderrow {
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: center;
}

.prc-11__count {
  font-size: 14px;
  font-weight: 600;
  color: #9fb0a0;
}

.prc-11__count output {
  font-size: 20px;
  font-weight: 800;
  color: var(--accent);
  margin-right: 4px;
}

.prc-11 input[type=range] {
  --pct: 9%;
  width: min(420px,86vw);
  height: 6px;
  border-radius: 99px;
  appearance: none;
  -webkit-appearance: none;
  background: linear-gradient(to right,var(--accent) var(--pct),#28322a var(--pct));
  cursor: pointer;
}

.prc-11 input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: #fff;
  border: 5px solid var(--accent);
  box-shadow: 0 4px 12px rgba(0,0,0,.4);
  transition: transform .15s;
}

.prc-11 input[type=range]::-webkit-slider-thumb:hover {
  transform: scale(1.15);
}

.prc-11 input[type=range]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #fff;
  border: 5px solid var(--accent);
}

.prc-11 input[type=range]:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 6px;
}

.prc-11__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit,minmax(220px,1fr));
  gap: 18px;
  width: min(880px,100%);
  margin: 0 auto;
}

.prc-11__card {
  position: relative;
  background: #161d17;
  border: 1.5px solid #263028;
  border-radius: 18px;
  padding: 28px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  transition: border-color .35s,box-shadow .35s;
}

.prc-11__card.prc-11__best {
  border-color: var(--accent);
  box-shadow: 0 18px 44px -26px oklch(0.62 0.18 145/.6);
}

.prc-11__best-badge {
  position: absolute;
  top: -11px;
  right: 20px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 99px;
  background: var(--accent);
  color: #0c130d;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity .35s,transform .35s;
}

.prc-11__card.prc-11__best .prc-11__best-badge {
  opacity: 1;
  transform: translateY(0);
}

.prc-11__card h3 {
  font-size: 14px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #93a394;
}

.prc-11__total {
  font-size: 40px;
  font-weight: 800;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}

.prc-11__total small {
  font-size: 15px;
  font-weight: 600;
  color: #93a394;
}

.prc-11__math {
  font-size: 12px;
  color: #7c8c7d;
  min-height: 16px;
  margin-bottom: 10px;
  font-variant-numeric: tabular-nums;
}

.prc-11__card ul {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 13.5px;
  color: #bccabc;
  flex: 1;
  margin-bottom: 18px;
}

.prc-11__card li::before {
  content: "✓";
  color: var(--accent);
  font-weight: 700;
  margin-right: 8px;
}

.prc-11__cta {
  text-align: center;
  text-decoration: none;
  font-weight: 700;
  font-size: 13.5px;
  padding: 12px;
  border-radius: 11px;
  border: 1px solid #2c382e;
  color: #e9efe9;
  transition: border-color .2s,background .2s;
}

.prc-11__cta:hover {
  border-color: var(--accent);
}

.prc-11__card.prc-11__best .prc-11__cta {
  background: var(--accent);
  border-color: var(--accent);
  color: #0c130d;
}

.prc-11__cta:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.prc-11__live {
  position: absolute;
  width: 1px;
  height: 1px;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

@media (prefers-reduced-motion: reduce) {
  .prc-11__card,
    .prc-11__best-badge {
    transition-duration: .01s;
  }
}
```

## JavaScript
```js
(() => {
  const root = document.querySelector('.prc-11');
  const range = root.querySelector('#prc-11-range');
  const out = root.querySelector('#prc-11-out');
  const live = root.querySelector('#prc-11-live');
  const cards = [...root.querySelectorAll('.prc-11__card')];
  const TIERS = [
    { name: 'Starter', base: 0, seat: 7 },
    { name: 'Team', base: 49, seat: 5 },
    { name: 'Business', base: 149, seat: 3.5 },
  ];
  const ANNOUNCE_MS = 600;
  let announceTimer;
  function update() {
    const seats = +range.value;
    out.textContent = seats;
    range.style.setProperty('--pct', ((seats - range.min) / (range.max - range.min) * 100) + '%');
    let bestIdx = 0, bestPrice = Infinity;
    TIERS.forEach((t, i) => {
      const total = Math.round(t.base + t.seat * seats);
      cards[i].querySelector('.prc-11__amount').textContent = '$' + total;
      cards[i].querySelector('.prc-11__math').textContent =
        (t.base ? '$' + t.base + ' base + ' : '') + '$' + t.seat + ' × ' + seats + ' seats';
      if (total < bestPrice) { bestPrice = total; bestIdx = i; }
    });
    cards.forEach((c, i) => c.classList.toggle('prc-11__best', i === bestIdx));
    clearTimeout(announceTimer);
    announceTimer = setTimeout(() => {
      live.textContent = seats + ' seats: best value is ' + TIERS[bestIdx].name + ' at $' + bestPrice + ' per month.';
    }, ANNOUNCE_MS);
  }
  range.addEventListener('input', update);
  update();
})();
```

How this works

A native <input type="range"> is the single source of truth. On input, JS multiplies each tier's per-seat rate by the seat count, writes the totals, and computes which tier is cheapest at that volume (Team plans have flat platform fees, so the winner changes with scale — realistic economics). The recommended tier gets a .prc-11__best class whose ring and badge fade in via CSS transitions; the DOM never rebuilds.

The slider's filled track is painted through a --pct custom property fed from JS into a linear-gradient. Totals sit inside a single aria-live="polite" region debounced to announce only when dragging pauses, avoiding a screen-reader firehose.

Make it yours

  • Edit tier economics in the TIERS array (base fee + per-seat rate).
  • Change the slider range/step on the input element — the JS adapts.
  • Debounce window for the aria-live announcement via ANNOUNCE_MS.
  • Swap seats for GB, API calls or renders — labels are data-driven.

Gotchas — read before shipping

  • Write prices with textContent into fixed-width slots so totals don't reflow card layout.
  • Don't announce every input event to aria-live — debounce, or screen readers read hundreds of numbers.
  • Style the range's WebKit and Gecko track/thumb pseudo-elements separately; they don't share syntax.

Browser support

ChromeSafariFirefoxEdge
111+15.4+113+111+

Floor set by oklch() as this demo is written. The core technique itself goes back further — Chrome 49+.

Range inputs, custom properties and transitions — universal. No cutting-edge CSS; the intelligence is 30 lines of vanilla JS.

Techniques used in this demo

Search CodeFronts

Loading…