Tailwind + JSMIT licensed

Tailwind Usage Slider Pricing Estimator

Drag the slider and the estimate, the request count and the recommended tier all move — with aria-valuetext announcing the whole result instead of a bare number. The port is a clean split: every layout and colour decision becomes a utility, and the four vendor pseudo-elements that paint a range track stay in CSS, where Tailwind has no variant and should not.

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="tps-05 w-full min-h-svh bg-tps05-bg font-tps05-sans text-tps05-ink px-[clamp(20px,4vw,40px)] py-[clamp(28px,5vw,56px)]" aria-label="Usage-based pricing">
  <header class="text-center mb-[clamp(24px,4vw,36px)]">
    <span class="inline-block mb-3.5 px-3 py-1.25 rounded-full bg-tps05-chip border border-tps05-edge2 text-[11px] font-bold tracking-[0.18em] uppercase text-tps05-acc">Pay-as-you-go</span>
    <h2 class="m-0 mb-2.5 text-[clamp(22px,3.4vw,32px)] font-bold tracking-[-0.02em] text-white text-pretty">Only pay for what you actually send.</h2>
    <p class="mx-auto my-0 max-w-[520px] text-sm/relaxed text-tps05-mut">Slide to estimate your monthly bill &mdash; we round up to the nearest tier so you&rsquo;re never surprised.</p>
  </header>
  <div class="max-w-[580px] mx-auto px-[clamp(20px,4vw,32px)] py-[clamp(24px,4vw,36px)] rounded-[18px] bg-linear-to-b from-tps05-card to-tps05-bg border border-tps05-edge shadow-[0_22px_60px_--theme(--color-tps05-glow)]">
    <div class="text-center pb-5.5 mb-5.5 border-b border-dashed border-tps05-edge">
      <span class="block text-[11px] tracking-[0.16em] uppercase text-tps05-mut" id="tps05-lbl">Estimated monthly cost</span>
      <span class="flex justify-center items-baseline gap-1 mt-2 mb-1.5 text-[64px] font-extrabold tracking-[-0.04em] text-transparent bg-linear-to-b from-tps05-acc to-tps05-acc2 bg-clip-text forced-colors:text-[CanvasText]"><em class="self-start pt-3 text-[28px] not-italic opacity-70">$</em><span id="tps05-price">12</span></span>
      <span class="text-[13px] text-tps05-mut"><span id="tps05-units">10K</span> requests on the <strong class="font-bold text-tps05-acc" id="tps05-tier">Starter</strong> tier</span>
    </div>
    <div class="px-1 pb-5.5">
      <label class="sr-only" for="tps05-range">Monthly request volume</label>
      <input type="range" id="tps05-range" min="0" max="100" step="1" value="10" aria-labelledby="tps05-lbl" aria-valuetext="10K requests, Starter tier, 12 dollars per month" class="tps-05__range w-full h-1.5 mt-2 mb-3 bg-transparent cursor-pointer [appearance:none] focus-visible:outline-none">
      <div class="flex justify-between font-tps05-mono text-[10.5px] text-tps05-dim" aria-hidden="true">
        <span>10K</span><span>100K</span><span>500K</span><span>1M</span><span>5M+</span>
      </div>
    </div>
    <ul class="list-none p-0 m-0 mb-5.5 text-[13px]/[1.9] text-tps05-body">
      <li><span class="inline-block w-5 text-tps05-acc" aria-hidden="true">&#10022;</span>No setup fee, no minimum spend</li>
      <li><span class="inline-block w-5 text-tps05-acc" aria-hidden="true">&#10022;</span>Auto-tier switching at every milestone</li>
      <li><span class="inline-block w-5 text-tps05-acc" aria-hidden="true">&#10022;</span>Usage cap option to prevent surprises</li>
    </ul>
    <a href="#start" class="block text-center px-4.5 py-3.25 rounded-[10px] text-sm font-bold tracking-[0.02em] text-white no-underline bg-linear-[135deg,var(--color-tps05-acc),var(--color-tps05-acc2)] shadow-[0_8px_22px_--theme(--color-tps05-glow2)] transition-transform duration-200 hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tps05-acc3 focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:hover:translate-y-0">Start with this plan &rarr;</a>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tps05-bg: #1a0a18;
  --color-tps05-bg: oklch(0.17 0.04 340);
  --color-tps05-card: #2a1227;
  --color-tps05-card: oklch(0.24 0.06 340);
  --color-tps05-edge: #5a2b4c;
  --color-tps05-edge: oklch(0.38 0.08 345);
  --color-tps05-edge2: #7a3a62;
  --color-tps05-edge2: oklch(0.46 0.1 345);
  --color-tps05-chip: #3a1830;
  --color-tps05-chip: oklch(0.28 0.07 345);
  --color-tps05-ink: #fde7f4;
  --color-tps05-ink: oklch(0.95 0.03 340);
  --color-tps05-body: #d8b8cc;
  --color-tps05-body: oklch(0.83 0.05 340);
  --color-tps05-mut: #c2a0b6;
  --color-tps05-mut: oklch(0.75 0.05 340);
  --color-tps05-dim: #9e7590;
  --color-tps05-dim: oklch(0.62 0.06 340);
  --color-tps05-acc: #f472b6;
  --color-tps05-acc: oklch(0.74 0.18 348);
  --color-tps05-acc2: #be185d;
  --color-tps05-acc2: oklch(0.5 0.19 358);
  --color-tps05-acc3: #f9a8d4;
  --color-tps05-acc3: oklch(0.83 0.11 348);
  --color-tps05-glow: #f472b61f;
  --color-tps05-glow: oklch(0.74 0.18 348 / 0.12);
  --color-tps05-glow2: #be185d59;
  --color-tps05-glow2: oklch(0.5 0.19 358 / 0.35);
  --font-tps05-sans: 'Inter', system-ui, sans-serif;
  --font-tps05-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, plus the four vendor pseudo-elements that
   paint a range input. Tailwind has no variant for ::-webkit-slider-thumb or
   ::-moz-range-progress and should not invent one — this is the boundary of what
   utilities can express, so the track and thumbs stay here and read the same
   @theme tokens as the markup. */

.tps-05 {
  width: 100%;
  min-height: 100svh;
}

.tps-05__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(90deg,
      var(--color-tps05-acc) 0%,
      var(--color-tps05-acc) var(--tps05-fill, 10%),
      var(--color-tps05-track) var(--tps05-fill, 10%),
      var(--color-tps05-track) 100%);
}

.tps-05__range::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--color-tps05-track);
}

.tps-05__range::-moz-range-progress {
  height: 6px;
  border-radius: 999px;
  background: var(--color-tps05-acc);
}

.tps-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 var(--color-tps05-acc);
  box-shadow: 0 4px 12px var(--color-tps05-glow2);
  cursor: grab;
}

.tps-05__range::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--color-tps05-acc);
  box-shadow: 0 4px 12px var(--color-tps05-glow2);
  cursor: grab;
}

.tps-05__range:focus-visible::-webkit-slider-thumb {
  outline: 3px solid var(--color-tps05-acc3);
  outline-offset: 2px;
}

.tps-05__range:focus-visible::-moz-range-thumb {
  outline: 3px solid var(--color-tps05-acc3);
  outline-offset: 2px;
}
/* Not an @theme entry on purpose: the unfilled track colour is never wanted as a
   bg-/text-/border- utility. */

.tps-05 {
  --color-tps05-track: oklch(0.28 0.05 340);
}
(function () {
  document.querySelectorAll('.tps-05').forEach(function (root) {
    if (root.dataset.bound) return; root.dataset.bound = '1';
    var range = root.querySelector('#tps05-range'),
        priceEl = root.querySelector('#tps05-price'),
        unitsEl = root.querySelector('#tps05-units'),
        tierEl = root.querySelector('#tps05-tier');
    if (!range) return;
    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 fmt(k) { return k >= 1000 ? (k / 1000).toFixed(k % 1000 ? 1 : 0) + 'M' : k.toLocaleString() + 'K'; }
    function update() {
      var v = parseInt(range.value, 10);
      var t = tiers[v < 25 ? 0 : v < 50 ? 1 : v < 75 ? 2 : v < 95 ? 3 : 4];
      priceEl.textContent = t.price.toLocaleString();
      unitsEl.textContent = fmt(t.units);
      tierEl.textContent = t.name;
      range.style.setProperty('--tps05-fill', v + '%');
      range.setAttribute('aria-valuetext', fmt(t.units) + ' requests, ' + t.name + ' tier, ' + t.price + ' dollars per month');
    }
    range.addEventListener('input', update);
    update();
  });
})();

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tps05-bg: #1a0a18;
  --color-tps05-bg: oklch(0.17 0.04 340);
  --color-tps05-card: #2a1227;
  --color-tps05-card: oklch(0.24 0.06 340);
  --color-tps05-edge: #5a2b4c;
  --color-tps05-edge: oklch(0.38 0.08 345);
  --color-tps05-edge2: #7a3a62;
  --color-tps05-edge2: oklch(0.46 0.1 345);
  --color-tps05-chip: #3a1830;
  --color-tps05-chip: oklch(0.28 0.07 345);
  --color-tps05-ink: #fde7f4;
  --color-tps05-ink: oklch(0.95 0.03 340);
  --color-tps05-body: #d8b8cc;
  --color-tps05-body: oklch(0.83 0.05 340);
  --color-tps05-mut: #c2a0b6;
  --color-tps05-mut: oklch(0.75 0.05 340);
  --color-tps05-dim: #9e7590;
  --color-tps05-dim: oklch(0.62 0.06 340);
  --color-tps05-acc: #f472b6;
  --color-tps05-acc: oklch(0.74 0.18 348);
  --color-tps05-acc2: #be185d;
  --color-tps05-acc2: oklch(0.5 0.19 358);
  --color-tps05-acc3: #f9a8d4;
  --color-tps05-acc3: oklch(0.83 0.11 348);
  --color-tps05-glow: #f472b61f;
  --color-tps05-glow: oklch(0.74 0.18 348 / 0.12);
  --color-tps05-glow2: #be185d59;
  --color-tps05-glow2: oklch(0.5 0.19 358 / 0.35);
  --font-tps05-sans: 'Inter', system-ui, sans-serif;
  --font-tps05-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, plus the four vendor pseudo-elements that
   paint a range input. Tailwind has no variant for ::-webkit-slider-thumb or
   ::-moz-range-progress and should not invent one — this is the boundary of what
   utilities can express, so the track and thumbs stay here and read the same
   @theme tokens as the markup. */

.tps-05 {
  width: 100%;
  min-height: 100svh;
}

.tps-05__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(90deg,
      var(--color-tps05-acc) 0%,
      var(--color-tps05-acc) var(--tps05-fill, 10%),
      var(--color-tps05-track) var(--tps05-fill, 10%),
      var(--color-tps05-track) 100%);
}

.tps-05__range::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--color-tps05-track);
}

.tps-05__range::-moz-range-progress {
  height: 6px;
  border-radius: 999px;
  background: var(--color-tps05-acc);
}

.tps-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 var(--color-tps05-acc);
  box-shadow: 0 4px 12px var(--color-tps05-glow2);
  cursor: grab;
}

.tps-05__range::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--color-tps05-acc);
  box-shadow: 0 4px 12px var(--color-tps05-glow2);
  cursor: grab;
}

.tps-05__range:focus-visible::-webkit-slider-thumb {
  outline: 3px solid var(--color-tps05-acc3);
  outline-offset: 2px;
}

.tps-05__range:focus-visible::-moz-range-thumb {
  outline: 3px solid var(--color-tps05-acc3);
  outline-offset: 2px;
}
/* Not an @theme entry on purpose: the unfilled track colour is never wanted as a
   bg-/text-/border- utility. */

.tps-05 {
  --color-tps05-track: oklch(0.28 0.05 340);
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The clearest boundary case in the collection: six pseudo-element rules stay in CSS because ::-webkit-slider-thumb and ::-moz-range-progress have no variant form — and they read @theme tokens, so re-theming still happens in one place.
  • Everything else moved: the card gradient, the 64px gradient numeral, the tick row, the feature list and the CTA are all utilities now, including the two glow shadows via --theme(--color-tps05-glow…).
  • The unfilled track colour is deliberately NOT an @theme entry — nothing wants bg-tps05-track. It sits in the root rule as a plain custom property, which is the rule the collection follows everywhere.
  • --tps05-fill is written by the script and read by the track gradient: a live value cannot be a class, so it becomes a property.
  • The hand-written .ps-05__sr clip rect becomes sr-only.
  • The script now scopes itself with document.querySelectorAll('.tps-05') and a data-bound guard, so two copies on one page each get their own controller.
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 demo 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: Tailwind Usage Slider Pricing Estimator
Source: https://codefronts.com/layouts/tailwind-css-pricing-sections/tailwind-usage-slider-pricing-estimator/

Drag the slider and the estimate, the request count and the recommended tier all move &mdash; with aria-valuetext announcing the whole result instead of a bare number. The port is a clean split: every layout and colour decision becomes a utility, and the four vendor pseudo-elements that paint a range track stay in CSS, where Tailwind has no variant and should not.
## HTML
```html
<section class="tps-05 w-full min-h-svh bg-tps05-bg font-tps05-sans text-tps05-ink px-[clamp(20px,4vw,40px)] py-[clamp(28px,5vw,56px)]" aria-label="Usage-based pricing">
  <header class="text-center mb-[clamp(24px,4vw,36px)]">
    <span class="inline-block mb-3.5 px-3 py-1.25 rounded-full bg-tps05-chip border border-tps05-edge2 text-[11px] font-bold tracking-[0.18em] uppercase text-tps05-acc">Pay-as-you-go</span>
    <h2 class="m-0 mb-2.5 text-[clamp(22px,3.4vw,32px)] font-bold tracking-[-0.02em] text-white text-pretty">Only pay for what you actually send.</h2>
    <p class="mx-auto my-0 max-w-[520px] text-sm/relaxed text-tps05-mut">Slide to estimate your monthly bill &mdash; we round up to the nearest tier so you&rsquo;re never surprised.</p>
  </header>
  <div class="max-w-[580px] mx-auto px-[clamp(20px,4vw,32px)] py-[clamp(24px,4vw,36px)] rounded-[18px] bg-linear-to-b from-tps05-card to-tps05-bg border border-tps05-edge shadow-[0_22px_60px_--theme(--color-tps05-glow)]">
    <div class="text-center pb-5.5 mb-5.5 border-b border-dashed border-tps05-edge">
      <span class="block text-[11px] tracking-[0.16em] uppercase text-tps05-mut" id="tps05-lbl">Estimated monthly cost</span>
      <span class="flex justify-center items-baseline gap-1 mt-2 mb-1.5 text-[64px] font-extrabold tracking-[-0.04em] text-transparent bg-linear-to-b from-tps05-acc to-tps05-acc2 bg-clip-text forced-colors:text-[CanvasText]"><em class="self-start pt-3 text-[28px] not-italic opacity-70">$</em><span id="tps05-price">12</span></span>
      <span class="text-[13px] text-tps05-mut"><span id="tps05-units">10K</span> requests on the <strong class="font-bold text-tps05-acc" id="tps05-tier">Starter</strong> tier</span>
    </div>
    <div class="px-1 pb-5.5">
      <label class="sr-only" for="tps05-range">Monthly request volume</label>
      <input type="range" id="tps05-range" min="0" max="100" step="1" value="10" aria-labelledby="tps05-lbl" aria-valuetext="10K requests, Starter tier, 12 dollars per month" class="tps-05__range w-full h-1.5 mt-2 mb-3 bg-transparent cursor-pointer [appearance:none] focus-visible:outline-none">
      <div class="flex justify-between font-tps05-mono text-[10.5px] text-tps05-dim" aria-hidden="true">
        <span>10K</span><span>100K</span><span>500K</span><span>1M</span><span>5M+</span>
      </div>
    </div>
    <ul class="list-none p-0 m-0 mb-5.5 text-[13px]/[1.9] text-tps05-body">
      <li><span class="inline-block w-5 text-tps05-acc" aria-hidden="true">&#10022;</span>No setup fee, no minimum spend</li>
      <li><span class="inline-block w-5 text-tps05-acc" aria-hidden="true">&#10022;</span>Auto-tier switching at every milestone</li>
      <li><span class="inline-block w-5 text-tps05-acc" aria-hidden="true">&#10022;</span>Usage cap option to prevent surprises</li>
    </ul>
    <a href="#start" class="block text-center px-4.5 py-3.25 rounded-[10px] text-sm font-bold tracking-[0.02em] text-white no-underline bg-linear-[135deg,var(--color-tps05-acc),var(--color-tps05-acc2)] shadow-[0_8px_22px_--theme(--color-tps05-glow2)] transition-transform duration-200 hover:-translate-y-px focus-visible:outline-2 focus-visible:outline-tps05-acc3 focus-visible:outline-offset-3 motion-reduce:transition-none motion-reduce:hover:translate-y-0">Start with this plan &rarr;</a>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tps05-bg: #1a0a18;
  --color-tps05-bg: oklch(0.17 0.04 340);
  --color-tps05-card: #2a1227;
  --color-tps05-card: oklch(0.24 0.06 340);
  --color-tps05-edge: #5a2b4c;
  --color-tps05-edge: oklch(0.38 0.08 345);
  --color-tps05-edge2: #7a3a62;
  --color-tps05-edge2: oklch(0.46 0.1 345);
  --color-tps05-chip: #3a1830;
  --color-tps05-chip: oklch(0.28 0.07 345);
  --color-tps05-ink: #fde7f4;
  --color-tps05-ink: oklch(0.95 0.03 340);
  --color-tps05-body: #d8b8cc;
  --color-tps05-body: oklch(0.83 0.05 340);
  --color-tps05-mut: #c2a0b6;
  --color-tps05-mut: oklch(0.75 0.05 340);
  --color-tps05-dim: #9e7590;
  --color-tps05-dim: oklch(0.62 0.06 340);
  --color-tps05-acc: #f472b6;
  --color-tps05-acc: oklch(0.74 0.18 348);
  --color-tps05-acc2: #be185d;
  --color-tps05-acc2: oklch(0.5 0.19 358);
  --color-tps05-acc3: #f9a8d4;
  --color-tps05-acc3: oklch(0.83 0.11 348);
  --color-tps05-glow: #f472b61f;
  --color-tps05-glow: oklch(0.74 0.18 348 / 0.12);
  --color-tps05-glow2: #be185d59;
  --color-tps05-glow2: oklch(0.5 0.19 358 / 0.35);
  --font-tps05-sans: 'Inter', system-ui, sans-serif;
  --font-tps05-mono: 'JetBrains Mono', ui-monospace, monospace;
}
/* Root rule: the full-bleed contract, plus the four vendor pseudo-elements that
   paint a range input. Tailwind has no variant for ::-webkit-slider-thumb or
   ::-moz-range-progress and should not invent one — this is the boundary of what
   utilities can express, so the track and thumbs stay here and read the same
   @theme tokens as the markup. */

.tps-05 {
  width: 100%;
  min-height: 100svh;
}

.tps-05__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: 999px;
  background: linear-gradient(90deg,
      var(--color-tps05-acc) 0%,
      var(--color-tps05-acc) var(--tps05-fill, 10%),
      var(--color-tps05-track) var(--tps05-fill, 10%),
      var(--color-tps05-track) 100%);
}

.tps-05__range::-moz-range-track {
  height: 6px;
  border-radius: 999px;
  background: var(--color-tps05-track);
}

.tps-05__range::-moz-range-progress {
  height: 6px;
  border-radius: 999px;
  background: var(--color-tps05-acc);
}

.tps-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 var(--color-tps05-acc);
  box-shadow: 0 4px 12px var(--color-tps05-glow2);
  cursor: grab;
}

.tps-05__range::-moz-range-thumb {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--color-tps05-acc);
  box-shadow: 0 4px 12px var(--color-tps05-glow2);
  cursor: grab;
}

.tps-05__range:focus-visible::-webkit-slider-thumb {
  outline: 3px solid var(--color-tps05-acc3);
  outline-offset: 2px;
}

.tps-05__range:focus-visible::-moz-range-thumb {
  outline: 3px solid var(--color-tps05-acc3);
  outline-offset: 2px;
}
/* Not an @theme entry on purpose: the unfilled track colour is never wanted as a
   bg-/text-/border- utility. */

.tps-05 {
  --color-tps05-track: oklch(0.28 0.05 340);
}
```

## JavaScript
```js
(function () {
  document.querySelectorAll('.tps-05').forEach(function (root) {
    if (root.dataset.bound) return; root.dataset.bound = '1';
    var range = root.querySelector('#tps05-range'),
        priceEl = root.querySelector('#tps05-price'),
        unitsEl = root.querySelector('#tps05-units'),
        tierEl = root.querySelector('#tps05-tier');
    if (!range) return;
    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 fmt(k) { return k >= 1000 ? (k / 1000).toFixed(k % 1000 ? 1 : 0) + 'M' : k.toLocaleString() + 'K'; }
    function update() {
      var v = parseInt(range.value, 10);
      var t = tiers[v < 25 ? 0 : v < 50 ? 1 : v < 75 ? 2 : v < 95 ? 3 : 4];
      priceEl.textContent = t.price.toLocaleString();
      unitsEl.textContent = fmt(t.units);
      tierEl.textContent = t.name;
      range.style.setProperty('--tps05-fill', v + '%');
      range.setAttribute('aria-valuetext', fmt(t.units) + ' requests, ' + t.name + ' tier, ' + t.price + ' dollars per month');
    }
    range.addEventListener('input', update);
    update();
  });
})();
```

How this works

What a range input costs in utilities. Tailwind styles the element, not its shadow parts:

<input type="range" class="w-full h-1.5 my-2 mb-3 bg-transparent cursor-pointer
  [appearance:none] focus-visible:outline-none">

The runnable track, the -moz progress bar and both thumbs live in the stylesheet. There is no ::-webkit-slider-thumb variant in v4, and inventing one with an arbitrary selector would be worse than the four CSS rules it replaces — so this demo keeps them, and that is the port's most useful lesson: utilities do not have to win everywhere.

The fill is a custom property, not a second element. The script writes --tps05-fill: 42% onto the input; the track gradient has two colour stops reading it. Static class, live value — the same pattern as the per-item index, and the only way a scrubbing value can drive paint without the compiler needing to know it.

The script writes text and one property. It maps the 0–100 position to five tiers, writes three text nodes, sets --tps05-fill and updates aria-valuetext. It touches no class and no style beyond that variable, so restyling never means reopening the JS.

The announcement is the accessibility work. A raw range announces “42”, which is meaningless here. aria-valuetext replaces it with the sentence a sighted user reads off the card.

Make it yours

  • Tier table: the tiers array in the script is the whole pricing model — units, price, tier name. Add a sixth stop and extend the breakpoint ladder in update().
  • Track colours: the two gradient stops live in the .tps-05 rule with the track. Recolour --color-tps05-acc and both thumbs follow, because they read the same token.
  • Thumb size: width/height plus the margin-top that re-centres it on the 6px track — keep the offset at half the difference or the thumb rides high in Chrome.
  • Snap to tiers: add step=&quot;25&quot; and drop the breakpoint ladder; the estimate then clicks between tiers instead of sliding.
  • Light mode: invert --color-tps05-bg/--color-tps05-ink and darken the accent one step; the 12% glow shadow needs roughly double the alpha to stay visible on cream.

Gotchas — read before shipping

  • [appearance:none] is required on the input itself or Chrome ignores every track rule. Keep it as a utility so nobody deletes it while &ldquo;cleaning up&rdquo; the CSS.
  • Do not try to express ::-webkit-slider-thumb as an arbitrary variant. There is no supported form, and a hand-rolled one breaks on the next compiler release.
  • Firefox needs ::-moz-range-progress for the filled portion — the -webkit gradient trick does nothing there. Both are in the stylesheet for a reason.
  • aria-valuetext must be updated on every input event. Update it only on change and screen-reader users hear stale numbers throughout the drag.
  • The gradient price text needs the forced-colors: fallback, exactly like demo 02 — transparent text with no background paints nothing in High Contrast Mode.
  • Keep the thumb at 22px. Below ~20px it fails the 24×24 target size of WCAG 2.5.8 for a control that is dragged.
  • The script writes --tps05-fill as a style property, not a class. A class-based fill would need 101 statically-known classes.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Range styling is split by engine (::-webkit-slider-* vs ::-moz-range-*) and has been stable in both for years; the standards-track ::slider-thumb is not shippable yet. The floor quoted here is Tailwind v4's own generated sheet.

Techniques used in this demo

Theme tokensset in @theme, used by a utility

Search CodeFronts

Loading…