How to build a pricing slider - HTML & Vanilla JS
Tags: html, javascript, tutorial, showdev
Published: 2020-03-16 16:35:33

If you are selling *pay as you go* or *subscription* plans, there might be chances that you need a landing page with a pricing table controlled by a [range slider](https://www.w3schools.com/howto/howto_js_rangeslider.asp) - just like in the example below 👇
{% codesandbox 55sxn %}
I've recently built it for our **[landing page templates at Cruip](https://cruip.com/)**, so I thought to write an article series to show you how I developed it (in HTML, React, and Vue), and what process I followed to implement the trickiest parts.

Let’s start with the **HTML and JavaScript** version, then we will cover React and Vue ones in the next articles!

##Creating the HTML structure
I've created a very basic HTML structure, with some ready-made CSS from the [Cruip framework](https://dev.to/cruip/5-reasons-why-i-built-a-css-framework-and-what-i-ve-learned-5cb2).
```html
<div class="pricing">

    <div class="pricing-slider">
        <label class="form-slider">
            <span>How many users do you have?</span>
            <input type="range" />
        </label>
        <div class="pricing-slider-value"></div>
    </div>

    <div class="pricing-items">
        <div class="pricing-item">
            <div class="pricing-item-inner">
                <div class="pricing-item-content">
                    <div class="pricing-item-header">
                        <div class="pricing-item-title">Basic</div>
                        <div class="pricing-item-price">
                            <span class="pricing-item-price-currency">$</span>
                            <span class="pricing-item-price-amount">13</span>
                            <span class="pricing-item-price-after">/m</span>
                        </div>
                    </div>
                    <div class="pricing-item-features">
                        <ul class="pricing-item-features-list">
                            <li class="is-checked">Excepteur sint occaecat</li>
                            <li class="is-checked">Excepteur sint occaecat</li>
                            <li class="is-checked">Excepteur sint occaecat</li>
                            <li>Excepteur sint occaecat</li>
                            <li>Excepteur sint occaecat</li>
                        </ul>
                    </div>
                </div>
                <div class="pricing-item-cta">
                    <a class="button" href="#">Buy Now</a>
                </div>
            </div>
        </div>
    </div>
</div>
```
Notice that we have **input** ⬇️ and **output** ⬆️ elements.

![Elements x-ray](https://dev-to-uploads.s3.amazonaws.com/i/vfhzou9yfkhlpc9ioics.png)

####Input elements
* The `<input type="range" />` element, i.e. the slider control
* The `<div class="pricing-slider-value">` element, into which we will write the current slider value

####Output elements
We can have multiple pricing tabs, which means multiple outputs. Each output consists of a `<div class="pricing-item-price">` element, that contains 3 more elements:
* `<span class="pricing-item-price-currency">` for the currency sign
* `<span class="pricing-item-price-amount">` for the amount
* `<span class="pricing-item-price-after">` for any other information, such as the billing period

And here is the result 👇
{% codesandbox f3bxf %}

##Shaping input and output data
We need to design our **data scheme** now. I've defined a range of slider values (*input*) and the corresponding price values (*output*).

| Key | Slider value | Price, currency | Price, amount | Price, after |
|-----|:------------:|:---------------:|:-------------:|-------------:|
| 0   | 1,000        |                 |Free           |              |
| 1   | 1,250        | $               |13             |/m            |
| 2   | 1,500        | $               |17             |/m            |
| 3   | 2,000        | $               |21             |/m            |
| 4   | 2,500        | $               |25             |/m            |
| 5   | 3,500        | $               |42             |/m            |
| 6   | 6,000        | $               |58             |/m            |
| 7   | 15,000       | $               |117            |/m            |
| 8   | 50,000       | $               |208            |/m            |
| 9   | 50,000+      |                 |Contact us     |              |

Go on adding **input** and **output** data to HTML via data attributes.

Input data 👇
```html
<input
  type="range"
  data-price-input='{
      "0": "1,000",
      "1": "1,250",
      "2": "1,500",
      "3": "2,000",
      "4": "2,500",
      "5": "3,500",
      "6": "6,000",
      "7": "15,000",
      "8": "50,000",
      "9": "50,000+"                        
    }'
/>
```
Output data looks a little different for structure, since each value is not a string, but an array of strings.
```html
<div
  class="pricing-item-price"
  data-price-output='{
    "0": ["", "Free", ""],
    "1": ["$", "13", "/m"],
    "2": ["$", "17", "/m"],
    "3": ["$", "21", "/m"],
    "4": ["$", "25", "/m"],
    "5": ["$", "42", "/m"],
    "6": ["$", "58", "/m"],
    "7": ["$", "117", "/m"],
    "8": ["$", "208", "/m"],
    "9": ["", "Contact us", ""]
  }'
>
```

##Defining JavaScript variables

Since we might want to display more than one pricing slider on a page, let's collect all elements having `pricing-slider` as a class, and loop through them.

```js
const pricingSliders = document.querySelectorAll(".pricing-slider");

if (pricingSliders.length > 0) {
  for (let i = 0; i < pricingSliders.length; i++) {
    const pricingSlider = pricingSliders[i];
  }
}
```
Now that we have our pricing slider defined by a constant, we can move forward with **storing elements and data**, for both *input* and *output*.

To do that, we are going to create:
* a `pricingInput` object that contains stuff dealing with the range slider (the *input*)
* a `pricingOutput` variable, that contains *output* elements and data. It's an array because, as previously said, we might have more than one *output* 😉

```js
if (pricingSliders.length > 0) {
  for (let i = 0; i < pricingSliders.length; i++) {
    const pricingSlider = pricingSliders[i];

    // Build the input object
    const pricingInput = {
      el: pricingSlider.querySelector("input")
    };
    pricingInput.data = JSON.parse(
      pricingInput.el.getAttribute("data-price-input")
    );
    pricingInput.currentValEl = pricingSlider.querySelector(
      ".pricing-slider-value"
    );
    pricingInput.thumbSize = parseInt(
      window
        .getComputedStyle(pricingInput.currentValEl)
        .getPropertyValue("--thumb-size"),
      10
    );

    // Build the output array
    const pricingOutputEls = pricingSlider.parentNode.querySelectorAll(
      ".pricing-item-price"
    );
    const pricingOutput = [];
    for (let i = 0; i < pricingOutputEls.length; i++) {
      const pricingOutputEl = pricingOutputEls[i];
      const pricingOutputObj = {};
      pricingOutputObj.currency = pricingOutputEl.querySelector(
        ".pricing-item-price-currency"
      );
      pricingOutputObj.amount = pricingOutputEl.querySelector(
        ".pricing-item-price-amount"
      );
      pricingOutputObj.after = pricingOutputEl.querySelector(
        ".pricing-item-price-after"
      );
      pricingOutputObj.data = JSON.parse(
        pricingOutputEl.getAttribute("data-price-output")
      );
      pricingOutput.push(pricingOutputObj);
    }
  }
}
```

Let's see what's inside these objects 📦

| Variable                 | returns                         |
|--------------------------|--------------------------------:|
|`pricingInput.el`         |`<input type="range" />` elem |
|`pricingInput.data`       |`{0: "1,000", 1: "1,250", ... }` object |
|`pricingInput.currentValEl`|`<div class="pricing-slider-value">` elem|
|`pricingInput.thumbSize`   |`36` slider thumb size (parsed from CSS) |
|`pricingOutput[n].currency`|`<span class="pricing-item-price-currency">` elem |
|`pricingOutput[n].amount`|`<span class="pricing-item-price-amount">` elem |
|`pricingOutput[n].after`|`<span class="pricing-item-price-after">` elem |
|`pricingOutput[n].data`|`{0: ["", "Free", ""], ... }` object |

##Setting range slider attributes
Now we can proceed with setting the range slider `min`, `max`, and `value` attributes.

```js
if (pricingSliders.length > 0) {
  for (let i = 0; i < pricingSliders.length; i++) {
    const pricingSlider = pricingSliders[i];

    // [ ... previously defined variables ... ]

    // set input range min attribute (0)
    pricingInputEl.setAttribute("min", 0);
    // set input range max attribute (9, i.e. the number of values)
    pricingInputEl.setAttribute("max", Object.keys(priceInput).length - 1);
    // initial slider value (0, or any other value if assigned via HTML)
    !pricingInputEl.getAttribute("value") &&
      pricingInputEl.setAttribute("value", 0);
  }
}
```
Great! We have a range slider whose values go from 0 to 9 🙌

The next step is outputting the **slider value** (e.g. `1,000`) that corresponds to the **current range value** (e.g. `0`), into the `<div class="pricing-slider-value">` element.

To do that, we need to create a function to be invoked every time a user interacts with the slide. As obvious, we need to pass our input and output objects as arguments

```js
function handlePricingSlide(input, output) {
  // output the current slider value
  if (input.currentValEl)
    input.currentValEl.innerHTML = input.data[input.el.value];
}
```
Let's call the function 📢

```js
if (pricingSliders.length > 0) {
  for (let i = 0; i < pricingSliders.length; i++) {
    const pricingSlider = pricingSliders[i];

    // [ ... previously defined variables ... ]
    // [ ... previous range slider attributes assignment ... ]

    handlePricingSlider(pricingInput, pricingOutput);
    window.addEventListener("input", function() {
      handlePricingSlider(pricingInput, pricingOutput);
    });
  }
}
```
And here is the result 👇
{% codesandbox jdyv2 %}

##Binding input and output data with JavaScript

We have a working range slider, but it is still disconnected from the visualized price. It's time to bind **input slider values** with **output price data**.

```js
function handlePricingSlide(input, output) {
  // output the current slider value
  if (input.currentValEl)
    input.currentValEl.innerHTML = input.data[input.el.value];
  // update prices
  for (let i = 0; i < output.length; i++) {
    const outputObj = output[i];
    if (outputObj.currency) outputObj.currency.innerHTML = outputObj.data[input.el.value][0];
    if (outputObj.amount) outputObj.amount.innerHTML = outputObj.data[input.el.value][1];
    if (outputObj.after) outputObj.after.innerHTML = outputObj.data[input.el.value][2];
  }
}
```
We are basically looping through each `pricingOutput` object and outputting `currency`, `amount` and `after` values into the destination elements.
{% codesandbox mngiy %}

##Adjusting the slider value element position
Almost there. 🏁 We want the slider value to be following the slider thumb.

Let's create a function calculating the left value to be applied to the slider value element.

```js
function handleSliderValuePosition(input) {
  const multiplier = input.el.value / input.el.max;
  const thumbOffset = input.thumbSize * multiplier;
  const priceInputOffset =
    (input.thumbSize - input.currentValEl.clientWidth) / 2;
  input.currentValEl.style.left =
    input.el.clientWidth * multiplier - thumbOffset + priceInputOffset + "px";
}
```
The function determines the proper slider value position, so that the element is horizontally aligned with the slider thumb. Here is a visual representation of what the function does 👇

![Slider value alignment](https://dev-to-uploads.s3.amazonaws.com/i/t276kmhqsq3p81jz34je.png)

Notice that the thumb size value is parsed with the `getComputedStyle()` method (see the paragraph where we defined the JS variables). That way I can change the thumb size in the CSS, without having to change anything in my JavaScript file.

##Setting a default slider value
In case you want to set an initial slider value other than `Free`, you just need to add a `value="n"` attribute to the range slider.

For example, `<input type="range" value="1" />` will return a range slider with `1,000` as initial slider value.

##Conclusions
Here is the final result again. Click on *Open Sandbox* to see the full code.
{% codesandbox 55sxn %}

I hope you enjoyed this tutorial. If you want to see this in action here is a landing page template where it’s implemented 👉 **[Surface](https://cruip.com/demos/surface/)**

[![Pricing component from Surface template](https://dev-to-uploads.s3.amazonaws.com/i/o4jto7s2t69h5f5lelz7.png)](https://cruip.com/demos/surface/)