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.
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 π
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
Top comments (5)
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.
Top tier stuff, very well explained even for a JS noob like myself :D
thank you for sharing this article
I cannot see the sandbox code.
How do I do that?
yeah. what the heck