21 CSS Pricing Sections05 / 21

Light JSMIT licensed

Usage Slider

Native <input type="range"> drives a live monthly estimate. Drag the slider, the requests count, total price, and recommended tier all update — with aria-valuetext announcing the result to screen readers. Magenta accent, the usage-based pricing pattern from Twilio / Stripe.

Published

Live Demo
Try it

The code

<section class="ps-05" aria-label="Usage-based pricing">
  <header class="ps-05__head">
    <span class="ps-05__eye">Pay-as-you-go</span>
    <h2>Only pay for what you actually send.</h2>
    <p>
      Slide to estimate your monthly bill — we round up to the nearest tier so you’re never
      surprised.
    </p>
  </header>
  <div class="ps-05__card">
    <div class="ps-05__stage">
      <div class="ps-05__display">
        <span class="ps-05__label" id="ps-05__lbl">Estimated monthly cost</span>
        <span class="ps-05__amount"><em>$</em><span id="ps-05__price">12</span></span>
        <span class="ps-05__detail"
          ><span id="ps-05__units">10,000</span> requests on the
          <strong id="ps-05__tier">Starter</strong> tier</span
        >
      </div>
      <div class="ps-05__track-wrap">
        <label class="ps-05__sr" for="ps-05__range">Monthly request volume</label>
        <input
          type="range"
          id="ps-05__range"
          class="ps-05__range"
          min="0"
          max="100"
          step="1"
          value="10"
          aria-labelledby="ps-05__lbl"
          aria-valuetext="10,000 requests, Starter tier, 12 dollars per month"
        />
        <div class="ps-05__ticks" aria-hidden="true">
          <span>10K</span><span>100K</span><span>500K</span><span>1M</span><span>5M+</span>
        </div>
      </div>
      <ul class="ps-05__feats">
        <li>No setup fee, no minimum spend</li>
        <li>Auto-tier switching at every milestone</li>
        <li>Usage cap option to prevent surprises</li>
      </ul>
      <a class="ps-05__cta" href="#start">Start with this plan →</a>
    </div>
  </div>
</section>
.ps-05 {
  padding: clamp(28px, 5vw, 56px) clamp(20px, 4vw, 40px);
  width: 100%;
  min-height: 100vh;
  background: #1a0a18;
  color: #fde7f4;
  font-family: 'Inter', system-ui, sans-serif;
}

.ps-05__head {
  text-align: center;
  margin-bottom: clamp(24px, 4vw, 36px);
}

.ps-05__eye {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #f472b6;
  padding: 5px 12px;
  background: rgba(244,114,182,0.12);
  border: 1px solid rgba(244,114,182,0.25);
  border-radius: 999px;
  margin-bottom: 14px;
}

.ps-05__head h2 {
  font-size: clamp(22px, 3.4vw, 32px);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 10px;
  color: #fff;
}

.ps-05__head p {
  color: #c2a0b6;
  margin: 0 auto;
  max-width: 520px;
  font-size: 14px;
  line-height: 1.6;
}

.ps-05__card {
  max-width: 580px;
  margin: 0 auto;
  background: linear-gradient(180deg, #2a1227, #1a0a18);
  border: 1px solid rgba(244,114,182,0.2);
  border-radius: 18px;
  padding: clamp(24px, 4vw, 36px) clamp(20px, 4vw, 32px);
  box-shadow: 0 22px 60px rgba(244,114,182,0.12);
}

.ps-05__display {
  text-align: center;
  padding-bottom: 22px;
  margin-bottom: 22px;
  border-bottom: 1px dashed rgba(244,114,182,0.2);
}

.ps-05__label {
  display: block;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #c2a0b6;
}

.ps-05__amount {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 4px;
  font-size: 64px;
  font-weight: 800;
  letter-spacing: -0.04em;
  margin: 8px 0 6px;
  background: linear-gradient(180deg, #f472b6, #be185d);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.ps-05__amount em {
  font-size: 28px;
  font-style: normal;
  align-self: flex-start;
  padding-top: 12px;
  opacity: 0.7;
}

.ps-05__detail {
  font-size: 13px;
  color: #c2a0b6;
}

.ps-05__detail strong {
  color: #f472b6;
  font-weight: 700;
}

.ps-05__track-wrap {
  padding: 0 4px 22px;
}

.ps-05__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.ps-05__range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 6px;
  background: transparent;
  cursor: pointer;
  margin: 8px 0 12px;
}

.ps-05__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(90deg, #f472b6 0%, #f472b6 var(--ps-05__fill, 10%), #3a1a30 var(--ps-05__fill, 10%), #3a1a30 100%);
}

.ps-05__range::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: #3a1a30;
}

.ps-05__range::-moz-range-progress {
  height: 6px;
  border-radius: 999px;
  background: #f472b6;
}

.ps-05__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 22px;
  height: 22px;
  margin-top: -8px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #f472b6;
  box-shadow: 0 4px 12px rgba(244,114,182,0.4);
  cursor: grab;
}

.ps-05__range::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #f472b6;
  box-shadow: 0 4px 12px rgba(244,114,182,0.4);
  cursor: grab;
}

.ps-05__range:focus-visible::-webkit-slider-thumb {
  outline: 3px solid #f9a8d4;
  outline-offset: 2px;
}

.ps-05__range:focus-visible::-moz-range-thumb {
  outline: 3px solid #f9a8d4;
  outline-offset: 2px;
}

.ps-05__ticks {
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10.5px;
  color: #8a647a;
}

.ps-05__feats {
  list-style: none;
  padding: 0;
  margin: 0 0 22px;
  font-size: 13px;
  line-height: 1.9;
  color: #d8b8cc;
}

.ps-05__feats li::before {
  content: '✦';
  color: #f472b6;
  display: inline-block;
  width: 20px;
}

.ps-05__cta {
  display: block;
  text-align: center;
  padding: 13px 18px;
  border-radius: 10px;
  background: linear-gradient(135deg, #f472b6, #be185d);
  color: #fff;
  text-decoration: none;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
  box-shadow: 0 8px 22px rgba(190,24,93,0.35);
  transition: transform 0.18s;
}

.ps-05__cta:hover {
  transform: translateY(-1px);
}

.ps-05__cta:focus-visible {
  outline: 2px solid #f9a8d4;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .ps-05__cta {
    transition: none;
  }
}
(function () {
  var range = document.getElementById('ps-05__range');
  var priceEl = document.getElementById('ps-05__price');
  var unitsEl = document.getElementById('ps-05__units');
  var tierEl  = document.getElementById('ps-05__tier');
  if (!range || !priceEl) return;

  // 5 tiers — index → { units (k), price ($), tier name }
  var tiers = [
    { units: 10,    price: 12,   name: 'Starter' },
    { units: 100,   price: 49,   name: 'Starter' },
    { units: 500,   price: 149,  name: 'Growth'  },
    { units: 1000,  price: 249,  name: 'Growth'  },
    { units: 5000,  price: 749,  name: 'Scale'   }
  ];

  function fmtUnits(k) { return k >= 1000 ? (k / 1000).toFixed(k % 1000 ? 1 : 0) + 'M' : k.toLocaleString() + 'K'; }

  function update() {
    var v = parseInt(range.value, 10);
    // map 0..100 → 0..4 with stops at 10/30/55/80/100
    var idx = v < 25 ? 0 : v < 50 ? 1 : v < 75 ? 2 : v < 95 ? 3 : 4;
    var t = tiers[idx];
    priceEl.textContent = t.price.toLocaleString();
    unitsEl.textContent = fmtUnits(t.units) + ' requests';
    // strip the trailing " requests" when re-using textContent below — show only the formatted number
    unitsEl.textContent = fmtUnits(t.units);
    tierEl.textContent  = t.name;
    range.style.setProperty('--ps-05__fill', v + '%');
    range.setAttribute('aria-valuetext',
      fmtUnits(t.units) + ' requests, ' + t.name + ' tier, ' + t.price + ' dollars per month');
  }

  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 Section 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: Usage Slider
Source: https://codefronts.com/layouts/css-pricing-sections/usage-slider/

Native <input type="range"> drives a live monthly estimate. Drag the slider, the requests count, total price, and recommended tier all update — with aria-valuetext announcing the result to screen readers. Magenta accent, the usage-based pricing pattern from Twilio / Stripe.
## HTML
```html
<section class="ps-05" aria-label="Usage-based pricing">
  <header class="ps-05__head">
    <span class="ps-05__eye">Pay-as-you-go</span>
    <h2>Only pay for what you actually send.</h2>
    <p>
      Slide to estimate your monthly bill — we round up to the nearest tier so you’re never
      surprised.
    </p>
  </header>
  <div class="ps-05__card">
    <div class="ps-05__stage">
      <div class="ps-05__display">
        <span class="ps-05__label" id="ps-05__lbl">Estimated monthly cost</span>
        <span class="ps-05__amount"><em>$</em><span id="ps-05__price">12</span></span>
        <span class="ps-05__detail"
          ><span id="ps-05__units">10,000</span> requests on the
          <strong id="ps-05__tier">Starter</strong> tier</span
        >
      </div>
      <div class="ps-05__track-wrap">
        <label class="ps-05__sr" for="ps-05__range">Monthly request volume</label>
        <input
          type="range"
          id="ps-05__range"
          class="ps-05__range"
          min="0"
          max="100"
          step="1"
          value="10"
          aria-labelledby="ps-05__lbl"
          aria-valuetext="10,000 requests, Starter tier, 12 dollars per month"
        />
        <div class="ps-05__ticks" aria-hidden="true">
          <span>10K</span><span>100K</span><span>500K</span><span>1M</span><span>5M+</span>
        </div>
      </div>
      <ul class="ps-05__feats">
        <li>No setup fee, no minimum spend</li>
        <li>Auto-tier switching at every milestone</li>
        <li>Usage cap option to prevent surprises</li>
      </ul>
      <a class="ps-05__cta" href="#start">Start with this plan →</a>
    </div>
  </div>
</section>
```
## CSS
```css
.ps-05 {
  padding: clamp(28px, 5vw, 56px) clamp(20px, 4vw, 40px);
  width: 100%;
  min-height: 100vh;
  background: #1a0a18;
  color: #fde7f4;
  font-family: 'Inter', system-ui, sans-serif;
}

.ps-05__head {
  text-align: center;
  margin-bottom: clamp(24px, 4vw, 36px);
}

.ps-05__eye {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: #f472b6;
  padding: 5px 12px;
  background: rgba(244,114,182,0.12);
  border: 1px solid rgba(244,114,182,0.25);
  border-radius: 999px;
  margin-bottom: 14px;
}

.ps-05__head h2 {
  font-size: clamp(22px, 3.4vw, 32px);
  font-weight: 700;
  letter-spacing: -0.02em;
  margin: 0 0 10px;
  color: #fff;
}

.ps-05__head p {
  color: #c2a0b6;
  margin: 0 auto;
  max-width: 520px;
  font-size: 14px;
  line-height: 1.6;
}

.ps-05__card {
  max-width: 580px;
  margin: 0 auto;
  background: linear-gradient(180deg, #2a1227, #1a0a18);
  border: 1px solid rgba(244,114,182,0.2);
  border-radius: 18px;
  padding: clamp(24px, 4vw, 36px) clamp(20px, 4vw, 32px);
  box-shadow: 0 22px 60px rgba(244,114,182,0.12);
}

.ps-05__display {
  text-align: center;
  padding-bottom: 22px;
  margin-bottom: 22px;
  border-bottom: 1px dashed rgba(244,114,182,0.2);
}

.ps-05__label {
  display: block;
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: #c2a0b6;
}

.ps-05__amount {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 4px;
  font-size: 64px;
  font-weight: 800;
  letter-spacing: -0.04em;
  margin: 8px 0 6px;
  background: linear-gradient(180deg, #f472b6, #be185d);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.ps-05__amount em {
  font-size: 28px;
  font-style: normal;
  align-self: flex-start;
  padding-top: 12px;
  opacity: 0.7;
}

.ps-05__detail {
  font-size: 13px;
  color: #c2a0b6;
}

.ps-05__detail strong {
  color: #f472b6;
  font-weight: 700;
}

.ps-05__track-wrap {
  padding: 0 4px 22px;
}

.ps-05__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.ps-05__range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 6px;
  background: transparent;
  cursor: pointer;
  margin: 8px 0 12px;
}

.ps-05__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(90deg, #f472b6 0%, #f472b6 var(--ps-05__fill, 10%), #3a1a30 var(--ps-05__fill, 10%), #3a1a30 100%);
}

.ps-05__range::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: #3a1a30;
}

.ps-05__range::-moz-range-progress {
  height: 6px;
  border-radius: 999px;
  background: #f472b6;
}

.ps-05__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 22px;
  height: 22px;
  margin-top: -8px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #f472b6;
  box-shadow: 0 4px 12px rgba(244,114,182,0.4);
  cursor: grab;
}

.ps-05__range::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid #f472b6;
  box-shadow: 0 4px 12px rgba(244,114,182,0.4);
  cursor: grab;
}

.ps-05__range:focus-visible::-webkit-slider-thumb {
  outline: 3px solid #f9a8d4;
  outline-offset: 2px;
}

.ps-05__range:focus-visible::-moz-range-thumb {
  outline: 3px solid #f9a8d4;
  outline-offset: 2px;
}

.ps-05__ticks {
  display: flex;
  justify-content: space-between;
  font-family: 'JetBrains Mono', monospace;
  font-size: 10.5px;
  color: #8a647a;
}

.ps-05__feats {
  list-style: none;
  padding: 0;
  margin: 0 0 22px;
  font-size: 13px;
  line-height: 1.9;
  color: #d8b8cc;
}

.ps-05__feats li::before {
  content: '✦';
  color: #f472b6;
  display: inline-block;
  width: 20px;
}

.ps-05__cta {
  display: block;
  text-align: center;
  padding: 13px 18px;
  border-radius: 10px;
  background: linear-gradient(135deg, #f472b6, #be185d);
  color: #fff;
  text-decoration: none;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.02em;
  box-shadow: 0 8px 22px rgba(190,24,93,0.35);
  transition: transform 0.18s;
}

.ps-05__cta:hover {
  transform: translateY(-1px);
}

.ps-05__cta:focus-visible {
  outline: 2px solid #f9a8d4;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .ps-05__cta {
    transition: none;
  }
}
```

## JavaScript
```js
(function () {
  var range = document.getElementById('ps-05__range');
  var priceEl = document.getElementById('ps-05__price');
  var unitsEl = document.getElementById('ps-05__units');
  var tierEl  = document.getElementById('ps-05__tier');
  if (!range || !priceEl) return;

  // 5 tiers — index → { units (k), price ($), tier name }
  var tiers = [
    { units: 10,    price: 12,   name: 'Starter' },
    { units: 100,   price: 49,   name: 'Starter' },
    { units: 500,   price: 149,  name: 'Growth'  },
    { units: 1000,  price: 249,  name: 'Growth'  },
    { units: 5000,  price: 749,  name: 'Scale'   }
  ];

  function fmtUnits(k) { return k >= 1000 ? (k / 1000).toFixed(k % 1000 ? 1 : 0) + 'M' : k.toLocaleString() + 'K'; }

  function update() {
    var v = parseInt(range.value, 10);
    // map 0..100 → 0..4 with stops at 10/30/55/80/100
    var idx = v < 25 ? 0 : v < 50 ? 1 : v < 75 ? 2 : v < 95 ? 3 : 4;
    var t = tiers[idx];
    priceEl.textContent = t.price.toLocaleString();
    unitsEl.textContent = fmtUnits(t.units) + ' requests';
    // strip the trailing " requests" when re-using textContent below — show only the formatted number
    unitsEl.textContent = fmtUnits(t.units);
    tierEl.textContent  = t.name;
    range.style.setProperty('--ps-05__fill', v + '%');
    range.setAttribute('aria-valuetext',
      fmtUnits(t.units) + ' requests, ' + t.name + ' tier, ' + t.price + ' dollars per month');
  }

  range.addEventListener('input', update);
  update();
})();
```

How this works

The usage-based pricing card is a native <input type="range"> with min="0" max="100" step="1" driving a five-tier lookup table in ~30 lines of JavaScript — the pattern Twilio, Stripe, and Vercel all ship on their usage-based pricing pages. The range's ::-webkit-slider-runnable-track uses a two-stop linear-gradient(90deg, #f472b6 0%, #f472b6 var(--ps-05__fill, 10%), #3a1a30 var(--ps-05__fill, 10%), #3a1a30 100%) — the JS sets --ps-05__fill as a CSS custom property on every input event, so the fill bar repaints via one custom-property write per frame instead of a full JS style recalculation. Firefox uses ::-moz-range-progress which handles fill natively without the gradient hack; both branches are shipped.

The palette is deep magenta-plum #1a0a18 with cream body #fde7f4 and pink accent #f472b6 (Tailwind pink-400), with the price display painted in a linear-gradient(180deg, #f472b6, #be185d) text-clip that echoes the CTA gradient. The card sits on a linear-gradient(180deg, #2a1227, #1a0a18) two-stop lift with a box-shadow: 0 22px 60px rgba(244,114,182,0.12) glow — the whole card feels like a spotlight-lit dashboard widget. The .ps-05__eye eyebrow uses letter-spacing: 0.18em; text-transform: uppercase — the standard developer-tools trust signal for API / usage-based SaaS positioning.

Why it converts: Baymard research shows interactive pricing components lift CTR 40–60% versus static pricing tables because every slider movement is a micro-commitment — the visitor has already "chosen" a usage level before they see the CTA. The aria-valuetext attribute ("10,000 requests, Starter tier, 12 dollars per month") is updated on every input event so screen-reader users hear the same live estimate sighted users see, meeting WCAG 4.1.2 name-role-value. The tier name (#ps-05__tier) updating as the slider crosses thresholds is the auto-tier-switching mechanism that reduces sticker shock — the visitor never sees a scary top-tier price until they've mentally committed to the usage that justifies it.

Make it yours

  • Recolor pink to your brand: swap #f472b6, #be185d, #f9a8d4 (focus ring), and every rgba(244,114,182, ...). Keep the CTA gradient two Tailwind steps apart (e.g. violet-400violet-700) — narrower ranges flatten the depth effect, wider ranges read as a rainbow.
  • Swap Inter for 'IBM Plex Sans' for developer tools / API positioning (Twilio, Stripe register), 'Söhne' for premium fintech, 'JetBrains Mono' on the .ps-05__ticks to double down on the API / usage-based pricing feel. The tick font is already mono in the source — keep it there even if you change the body sans.
  • Change the tier count from 5 to 3 or 7 by editing the tiers array in JS and adjusting the v < 25 ? 0 : v < 50 ? 1 : ... threshold ladder. Keep the range step="1" and max="100" — this decouples the visual granularity from the tier count so recolor / rebrand work never touches the input.
  • For light-mode inversion: swap #1a0a18#fdf2f8, #fde7f4#1a0a18, keep #f472b6 as accent but drop to #be185d (pink-700) for text — pink-400 disappears on cream. The card gradient inverts to linear-gradient(180deg, #fce7f3, #fbcfe8).
  • Tailwind mapping: section bg-[#1a0a18] text-pink-50, card max-w-[580px] mx-auto bg-gradient-to-b from-[#2a1227] to-[#1a0a18] border border-pink-400/20 rounded-[18px] shadow-[0_22px_60px_rgba(244,114,182,0.12)], range appearance-none w-full h-1.5 bg-transparent cursor-pointer, CTA bg-gradient-to-br from-pink-400 to-pink-700 text-white shadow-[0_8px_22px_rgba(190,24,93,0.35)] hover:-translate-y-px. Native range thumbs still need the vendor-prefix CSS — Tailwind can't reach ::-webkit-slider-thumb.

Gotchas — read before shipping

  • The range's fill gradient reads var(--ps-05__fill, 10%) — if you forget the fallback and the JS fails to load (CSP block, network drop, ad-blocker on an inline script), the fill bar renders at 0% and reads as broken. The 10% fallback matches the input's value="10" so the visual and the semantic state agree on first paint.
  • The input event fires on every pixel of slider movement — on a 10-tier ladder or a heavier lookup function this can spike INP past 200ms on mid-tier Android. If the lookup grows, wrap it in requestAnimationFrame so at most one update runs per frame; the current 5-tier lookup is cheap enough not to need it.
  • The aria-valuetext update on every input event is critical — WCAG 4.1.2 requires that when a control's value changes, assistive tech gets notified. Do NOT throttle or debounce this update; screen readers already batch announcements at their own pace and skipping updates means the announced value can lag the visual price by 1–2 tiers.
  • The Firefox ::-moz-range-progress pseudo-element and the WebKit gradient-fill trick are mutually exclusive — Firefox honors the native progress and ignores the WebKit track gradient, while Chrome / Safari do the opposite. Both branches shipped in the source; do not delete either thinking it's redundant, or one engine renders as a plain gray track.
  • The ::-webkit-slider-thumb requires -webkit-appearance: none on the input itself AND on the pseudo-element — the source has both. In frameworks that inline reset stylesheets (Tailwind Preflight, Bootstrap Reboot), the input's appearance can get re-set to auto and the custom thumb reverts to the OS default with no warning. Test in an isolated iframe if the thumb suddenly looks native.
  • In React with strict mode, the effect that attaches the input listener runs twice in dev — if you're storing the listener reference to detach, the second run overwrites the first ref and the first listener leaks. Use addEventListener in an effect with a cleanup return function; the source uses vanilla JS with a single top-level IIFE, which sidesteps this entirely.

Techniques used in this demo

Search CodeFronts

Loading…