DEV Community

Cover image for How to build a pricing slider - HTML & Vanilla JS
Pasquale Vitiello for Cruip

Posted on • Updated on

How to build a pricing slider - HTML & Vanilla JS

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 - just like in the example below πŸ‘‡

I've recently built it for our landing page templates at Cruip, 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.

<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

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 πŸ‘‡

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 πŸ‘‡

<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.

<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.

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 πŸ˜‰
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.

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

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 πŸ“’

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 πŸ‘‡

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.

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.

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.

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

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.

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

Pricing component from Surface template

Top comments (5)

Collapse
 
bl1133 profile image
Bryan Lee • Edited

There is a typo when you write "pricingInputEl.setAttribute". It should be pricingInput.el.setAttribute. And right after it says Object.keys(priceInput) and it should be Object.keys(pricingInput.data). This confused me until I saw your codesandbox. Great tutorial it's helping me alot.

Collapse
 
mahenderson profile image
Mathias Henderson

Top tier stuff, very well explained even for a JS noob like myself :D

Collapse
 
fajarsiddiq profile image
Fajar Siddiq

thank you for sharing this article

Collapse
 
qabot profile image
QABOT

I cannot see the sandbox code.

How do I do that?

Collapse
 
spectrumvoipmegan profile image
spectrumvoip-megan

yeah. what the heck