Tutorials 4 min read

How to create a pricing slider with Tailwind CSS: JavaScript and Alpine.js

Build a pricing slider with Tailwind CSS, two ways: vanilla JavaScript or Alpine.js. Drag to pick pageviews and the price updates instantly.

Today we are building a pricing slider for your pricing page: drag it and the price updates based on the selected pageviews. We’ll build it twice, first with vanilla JavaScript, then with Alpine.js, so you can pick whichever fits your stack.

What is a pricing slider?

A pricing slider lets your customers pick a usage level and instantly see what it costs. In this very case the user selects a number of pageviews and we calculate the price from that, which is a great way to help people choose the right plan for their needs.

Use cases

  • Subscription plans: let users dial in their usage and see the matching tier.
  • Usage-based pricing: adjust the price dynamically from metrics like pageviews or seats.
  • Service packages: offer customizable packages with different levels of features or support.
  • Software licensing: price licenses by usage or functionality tiers.

The markup

  • <input type="range" id="pageviews" min="0" max="1000000" step="1000" />: the slider itself.
  • <input type="number" id="inputPageviews" />: a number field kept in sync with the slider.
  • <input type="text" id="price" readonly />: the read-only field that displays the calculated price.
  • <span id="pageviewsText"></span>: displays the formatted pageviews value.

Classes are omitted for brevity and clarity.

html
<div>
  <div>
    <input type="range" id="pageviews" min="0" max="1000000" step="1000" />
  </div>
  <div>
    <label for="pageviews">Pageviews</label>
    <input type="number" id="inputPageviews" />
  </div>
  <div>
    <p>
      <span>$ <input type="text" id="price" readonly /> </span>
    </p>
    <div>
      <label for="pageviews">Pageviews</label>
      <span id="pageviewsText"></span>
    </div>
    <p>This plan is tailored for small businesses and startups</p>
    <div>
      <button type="submit">Get access</button>
    </div>
    <p>Invoices and receipts available for easy company reimbursement</p>
  </div>
</div>

The script

  • document.addEventListener("DOMContentLoaded", function () {: waits for the DOM, then grabs the slider, the number input, the price field, and the pageviews text.
  • function calculatePrice(pageviews) {: returns (Math.ceil(pageviews / 1000) * 0.001 * 50).toFixed(2), the price for the selected pageviews.
  • function updatePriceAndPageviews(pageviews) {: writes the calculated price into the price field and the formatted pageviews into the text element.
  • function handleInput(event) {: keeps the slider and the number input in sync and updates the price whenever either one changes.
  • At the end we initialize both inputs to 0 and run the update once, so the component starts in a consistent state.
js
document.addEventListener("DOMContentLoaded", function () {
  const pageviewsInput = document.getElementById("pageviews");
  const inputPageviews = document.getElementById("inputPageviews");
  const priceInput = document.getElementById("price");
  const pageviewsText = document.getElementById("pageviewsText");

  function calculatePrice(pageviews) {
    return (Math.ceil(pageviews / 1000) * 0.001 * 50).toFixed(2);
  }

  function updatePriceAndPageviews(pageviews) {
    priceInput.value = calculatePrice(pageviews);
    pageviewsText.textContent = parseInt(pageviews, 10).toLocaleString();
  }

  function handleInput(event) {
    const pageviews = event.target.value;
    pageviewsInput.value = pageviews;
    inputPageviews.value = pageviews;
    updatePriceAndPageviews(pageviews);
  }

  pageviewsInput.addEventListener("input", handleInput);
  inputPageviews.addEventListener("input", handleInput);

  // Initialize with default value
  pageviewsInput.value = 0;
  inputPageviews.value = 0;
  updatePriceAndPageviews(0);
});

The Alpine.js version

Same component, no separate script: the state lives in x-data.

  • x-data="{ pageviews: 1000, price: 0 }": stores the pageviews and the price.
  • x-model="pageviews": binds both the slider and the number input to the same state.
  • @input="price = (Math.ceil(pageviews / 1000) * 0.001 * 50).toFixed(2)": recalculates the price on every change; the number input does the same with $event.target.value.
  • x-model="price" on the read-only field: displays the calculated price.
  • <span x-text="pageviews.toLocaleString()"></span>: displays the formatted pageviews value.

Classes are removed for brevity, but I’ll keep those classes relevant to the tutorial.

html
<div x-data="{ pageviews: 1000, price: 0 }" class="w-full">
  <div>
    <input type="range" id="pageviews" x-model="pageviews" min="1000"
    max="1000000" step="1000" @input="price = (Math.ceil(pageviews / 1000) *
    0.001 * 50).toFixed(2)" />
  </div>
  <div>
    <label for="pageviews">Pageviews</label>
    <input
      type="number"
      id="inputPageviews"
      x-model="pageviews"
      @input="price = (Math.ceil($event.target.value / 1000) * 0.001 * 50).toFixed(2)"
    />
  </div>

  <div>
    <p>
      <span>$<input type="text" id="price" x-model="price" readonly /></span>
    </p>

    <div>
      <label for="pageviews">Pageviews</label>
      <span x-text="pageviews.toLocaleString()"></span>
    </div>
    <p>This plan is tailored for small businesses and startups</p>
    <div>
      <button>Get access</button>
    </div>
    <p>Invoices and receipts available for easy company reimbursement</p>
  </div>
</div>

You can try the Alpine version separately: live demo and source code.

Which one should you use?

The Alpine version computes the price inline in the markup, which is handy when Alpine is already loaded. The vanilla version keeps the calculation in one small script and needs no dependency at all.

Conclusion

One slider, one formula, and synced inputs: that’s the whole component. Remember to make it fully accessible and keep the pricing logic clear to the user when implementing it on your project.

Hope you enjoyed this tutorial and have a great day!

/Michael Andreuzza