DEV Community

Cover image for Creating a Pricing Table with Range Slider using Tailwind CSS and Alpine.js
Cruip
Cruip

Posted on • Originally published at cruip.com

Creating a Pricing Table with Range Slider using Tailwind CSS and Alpine.js

Live Demo / Download

In the SaaS world, products or services are often offered at a set price, along with a discount for yearly subscriptions. This conventional approach is widely used and demonstrated in this gallery of the best pricing page examples. However, there are situations where the cost depends on quantity. This is known as "tiered pricing" and is commonly used in hosting, storage, APIs, SMS, and other services. Brevo is an example of this, where the pricing plans change depending on the number of emails sent each month. Such pricing structures are typically presented using a range slider, allowing users to adjust the quantity and see the corresponding price. The goal of this tutorial is to create some pricing tables with a range slider, inspired by the example of Brevo and using Tailwind CSS and Alpine.js. As always, we'll pay maximum attention to accessibility requirements, ensuring that our price table is user-friendly for everyone.

Creating the HTML

To structure our component, we'll reuse the pricing tables built in a previous tutorial. This way, we can focus on developing the range slider and its interaction with the prices to be displayed.

<div>

    <!-- Pricing slider -->

    <div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-2 items-start lg:max-w-[728px]">

        <!-- Pricing tab 1 -->
        <div class="h-full">                                
            <div class="relative flex flex-col h-full p-6 pb-10">
                <div class="h-8" aria-hidden="true"></div>
                <div class="mb-5">
                    <div class="text-slate-900 font-semibold mb-1">Starter</div>
                    <div class="inline-flex items-baseline mb-2">
                        <span class="text-slate-900 font-bold text-3xl">$</span>
                        <span class="text-slate-900 font-bold text-4xl">29</span>
                        <span class="text-slate-500 font-medium">/mo</span>
                    </div>
                    <div class="text-sm text-slate-500 mb-5">There are many variations available, but the majority have suffered.</div>
                    <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150" href="#0">
                        Purchase Plan
                    </a>
                </div>
                <div class="text-slate-900 text-sm font-medium mb-4">Includes:</div>
                <ul class="text-slate-600 text-sm space-y-3 grow">
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Unlimited placeholder texts</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Consectetur adipiscing elit</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Excepteur sint occaecat cupidatat</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Officia deserunt mollit anim</span>
                    </li>
                </ul>
            </div>
        </div>

        <!-- Pricing tab 2 -->
        <div class="h-full">
            <div class="relative flex flex-col h-full p-6 pb-10 rounded-2xl bg-slate-900 border border-slate-900 shadow shadow-slate-950/5">
                <div class="absolute top-0 right-0 mr-6 -mt-4">
                    <div class="inline-flex items-center text-xs font-semibold py-1.5 px-3 bg-emerald-500 text-white rounded-full shadow-sm shadow-slate-950/5">Most Popular</div>
                </div>
                <div class="h-8" aria-hidden="true">
                    <svg class="drop-shadow-[0_0_8px_rgba(224,154,19,0.7)]" xmlns="http://www.w3.org/2000/svg" width="24" height="26">
                        <defs>
                            <linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%">
                                <stop offset="0%" stop-color="#FBBF24" />
                                <stop offset="100%" stop-color="#F59E0B" />
                            </linearGradient>
                        </defs>
                        <path fill="url(#a)" fill-rule="evenodd" d="M15 0 0 16h11L9 26l15-16H13z" />
                    </svg>
                </div>
                <div class="mb-5">
                    <div class="text-slate-200 font-semibold mb-1">Business</div>
                    <div class="inline-flex items-baseline mb-2">
                        <span class="text-slate-200 font-bold text-3xl">$</span>
                        <span class="text-slate-200 font-bold text-4xl">49</span>
                        <span class="text-slate-500 font-medium">/mo</span>
                    </div>
                    <div class="text-sm text-slate-500 mb-5">There are many variations available, but the majority have suffered.</div>
                    <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                        Purchase Plan
                    </a>
                </div>
                <div class="text-slate-200 text-sm font-medium mb-4">Everything in Starter, plus:</div>
                <ul class="text-slate-400 text-sm space-y-3 grow">
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Unlimited placeholder texts</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Consectetur adipiscing elit</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Excepteur sint occaecat cupidatat</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Officia deserunt mollit anim</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Predefined chunks as necessary</span>
                    </li>
                </ul>
            </div>
        </div>

    </div>

</div>
Enter fullscreen mode Exit fullscreen mode

In the above code, we've have two pricing tables, one for the "Starter" plan and a visually distinct one in dark theme for the "Business" plan. To save time, we've pre-styled these components for you. Now let's see how to integrate the range slider and make it functional.

Creating the range slider

Before building this component, I took some time to think about whether to use a native range input or a custom slider with ARIA attributes. In the end, I chose first option. Although styling a range input can be tricky, it's easier than building a slider from scratch, especially when it comes to making it fully functional with JavaScript. Futhermore, we decided to use Alpine.js because we prefer quick solutions rather than diving into JavaScript complexities. By the end of this tutorial, you'll see that handling the whole logic was easier than styling the slider! Let's start by adding an input element to the page:

<div class="max-w-sm mx-auto lg:max-w-3xl space-y-3 mb-12 lg:mb-16">
    <div class="text-center text-sm text-slate-700 font-medium">10K contacts/month</div>
    <div class="relative flex items-center">
        <input type="range" min="0" max="4" aria-valuetext="10K contacts/month" aria-label="Pricing Slider">
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Currently, the slider doesn't have any styles. It looks differently in various browsers as it is a native HTML element. The purpose of this tutorial is to give the slider a custom and consistent style across all browsers.

Styling the slider thumb

Before continuing, let's carefully examine the slider design and strategize our approach: Anatomy of the pricing slider The slider thumb is essentially a white circle with a subtle shadow, measuring 20x20 pixels. We'll apply the appropriate style to the pseudo-element that represents it - ::-webkit-slider-thumb in Webkit-based browsers (Chrome, Safari, Edge, etc.) and ::-moz-range-thumb in Firefox. Let's outline the Tailwind classes to be added to the input element:

<div class="max-w-sm mx-auto lg:max-w-3xl space-y-3 mb-12 lg:mb-16">
    <div class="text-center text-sm text-slate-700 font-medium">10K contacts/month</div>
    <div class="relative flex items-center">
        <input class="
            relative appearance-none cursor-pointer w-full bg-transparent focus:outline-none
            [&::-webkit-slider-thumb]:appearance-none
            [&::-webkit-slider-thumb]:h-5
            [&::-webkit-slider-thumb]:w-5
            [&::-webkit-slider-thumb]:rounded-full
            [&::-webkit-slider-thumb]:bg-white
            [&::-webkit-slider-thumb]:shadow
            [&::-webkit-slider-thumb]:focus-visible:ring
            [&::-webkit-slider-thumb]:focus-visible:ring-indigo-300
            [&::-moz-range-thumb]:h-5
            [&::-moz-range-thumb]:w-5                            
            [&::-moz-range-thumb]:rounded-full
            [&::-moz-range-thumb]:bg-white
            [&::-moz-range-thumb]:border-none
            [&::-moz-range-thumb]:shadow
            [&::-moz-range-thumb]:focus-visible:ring
            [&::-moz-range-thumb]:focus-visible:ring-indigo-300                            
        " type="range" min="0" max="4" aria-valuetext="10K contacts/month" aria-label="Pricing Slider">
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

For those who prefer the traditional "semantic CSS" approach, the code may seem somewhat extensive at this point. This is a consequence of employing utility classes. If the multitude of classes within a single element feels overwhelming, you can always create a custom class and apply it to the input element. Note that we've also already styled the focus. So, when you use the Tab key, you can easily navigate to the slider and adjust the value using the right and left arrows. When the element is in focus, you'll see a nice indigo-colored focus ring that matches the slider's background color.

Styling the slider track

Let's go back to our original design and focus on the slider track now: it's a horizontal bar with rounded edges, measuring 6 pixels in height and having a very light gray color. There are two main complexities with this element:

  • The area to the left of the thumb is a linear gradient, starting with a light indigo and ending with a slightly darker indigo.
  • The entire track is marked with a tick at each step.

It's a real CSS challenge. We cannot use the pseudo-elements ::-webkit-slider-runnable-track and ::-moz-range-track because a single element doesn't allow us to incorporate all the details from the design. So, let's make native track transparent and add a new HTML element as a replacement:

<div class="max-w-sm mx-auto lg:max-w-3xl space-y-3 mb-12 lg:mb-16">
    <div class="text-center text-sm text-slate-700 font-medium">10K contacts/month</div>
    <div class="relative flex items-center">
        <div class="
            absolute left-2.5 right-2.5 h-1.5 bg-slate-200 rounded-full overflow-hidden
        " aria-hidden="true"></div>            
        <input class="
            relative appearance-none cursor-pointer w-full bg-transparent focus:outline-none
            [&::-webkit-slider-thumb]:appearance-none
            [&::-webkit-slider-thumb]:h-5
            [&::-webkit-slider-thumb]:w-5
            [&::-webkit-slider-thumb]:rounded-full
            [&::-webkit-slider-thumb]:bg-white
            [&::-webkit-slider-thumb]:shadow
            [&::-webkit-slider-thumb]:focus-visible:ring
            [&::-webkit-slider-thumb]:focus-visible:ring-indigo-300
            [&::-moz-range-thumb]:h-5
            [&::-moz-range-thumb]:w-5                            
            [&::-moz-range-thumb]:rounded-full
            [&::-moz-range-thumb]:bg-white
            [&::-moz-range-thumb]:border-none
            [&::-moz-range-thumb]:shadow
            [&::-moz-range-thumb]:focus-visible:ring
            [&::-moz-range-thumb]:focus-visible:ring-indigo-300                            
        " type="range" min="0" max="4" aria-valuetext="10K contacts/month" aria-label="Pricing Slider">
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

As you can see, we've added a <div< with the aria-hidden="true" attribute that hides it to screen readers. Next, we'll use the ::before pseudo-element to create the linear gradient and the ::after pseudo-element for the ticks. Let's start with the linear gradient:

<div class="
    absolute left-2.5 right-2.5 h-1.5 bg-slate-200 rounded-full overflow-hidden
    before:absolute
    before:inset-0
    before:bg-gradient-to-r
    before:from-indigo-300
    before:to-indigo-500
    before:[mask-image:_linear-gradient(to_right,theme(colors.white),theme(colors.white)_50%,transparent_50%)]                
" aria-hidden="true"></div>
Enter fullscreen mode Exit fullscreen mode

The fill bar spans the entire bar and is perfectly clipped in half using a mask - we'll see how to dinalically adjust the mask position based on the slider value later. Now, let's use the ::after pseudo-element to show the ticks:

<div class="
    absolute left-2.5 right-2.5 h-1.5 bg-slate-200 rounded-full overflow-hidden
    before:absolute
    before:inset-0
    before:bg-gradient-to-r
    before:from-indigo-300
    before:to-indigo-500
    before:[mask-image:_linear-gradient(to_right,theme(colors.white),theme(colors.white)_50%,transparent_50%)]
    after:absolute
    after:inset-0
    after:bg-[repeating-linear-gradient(to_right,transparent,transparent_calc(25%-1px),theme(colors.white/.7)_calc(25%-1px),theme(colors.white/.7)_calc(25%+1px))]
" aria-hidden="true"></div>
Enter fullscreen mode Exit fullscreen mode

We are using a repeating linear gradient for the ticks. The slider steps are 5, so the bar consists of 4 segments, each occupying 25% of the total length. To create the repeating gradient pattern for the ticks with a total width of 2 pixels, we'll follow this approach:

  • From 0% to 25% - 1, the color remains transparent.
  • From 25% - 1 to 25% + 1, the color is white with 70% opacity.

This way, we'll have 3 ticks at 25%, 50%, and 75% of the bar's length. Cool, isn't it? As obvious, the value 25% works as long as we have a range of 5 steps. We'll see later how to make this value dynamic.

Adding Alpine.js logic

So far, we've styled out range slider. Now, let's add Alpine.js to add interactivity. First, include the library in the document head:

<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

The next step is to add an x-data attribute to the element that contains both the range input and the pricing tables:

<div x-data="pricingSlider">

    <!-- Pricing slider -->
    <div class="max-w-sm mx-auto lg:max-w-3xl space-y-3 mb-12 lg:mb-16">
        <div class="text-center text-sm text-slate-700 font-medium">10K contacts/month</div>
            ...
Enter fullscreen mode Exit fullscreen mode

Now, immediately after the x-data="pricingSlider"> element, we'll add a <script> tag containing the JavaScript object with the logic:

<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('pricingSlider', () => ({
            value: 2,
            prices: [
                {
                    contacts: '1K',
                    plans: {
                        starter: '5',
                        business: '9',
                    }
                },
                {
                    contacts: '5K',
                    plans: {
                        starter: '19',
                        business: '29',
                    }
                },
                {
                    contacts: '10K',
                    plans: {
                        starter: '29',
                        business: '49',
                    }
                },
                {
                    contacts: '15K',
                    plans: {
                        starter: '39',
                        business: '59',
                    }
                },
                {
                    contacts: '1M',
                    plans: {
                        starter: '1,490',
                        business: '2,490',
                    }
                },
            ],
            segmentsWidth: '100%',
            progress: '0%',
            segments: 1,
            calculateProgress() {
                this.segmentsWidth = 100 / this.segments + '%'
                this.progress = 100 / this.segments * this.value + '%'
            },
            init() {
                this.segments = this.prices.length - 1
                this.calculateProgress()
                this.$watch('value', () => this.calculateProgress())
            },
        }))
    })
</script>
Enter fullscreen mode Exit fullscreen mode

A few things are going on in this code, so let's break it down:

  • The value property represents the current slider value, initialized to 2.
  • The prices property contains an array of objects, each representing a segment of the slider, with corresponding contact numbers and prices for "Starter" and "Business" plans.
  • The segmentsWidth property denotes the width of each slider, initialized to 100%.
  • The progress property represents the width of the fill bar, initialized to 0%.
  • The calculateProgress() method calculates the width of each segment (segmentsWidth) and the width of the fill bar (progress).
  • The init() method, executed at startup, invokes the calculateProgress() method. Additionally, it adds a watcher to the value property so that every time the slider value changes, the calculateProgress() method is called again.

Completing the slider functionality

Now we have everything we need to make the slider functional. Let's bind the slider value:

<input class="
    relative appearance-none cursor-pointer w-full bg-transparent focus:outline-none
    [&::-webkit-slider-thumb]:appearance-none
    [&::-webkit-slider-thumb]:h-5
    [&::-webkit-slider-thumb]:w-5
    [&::-webkit-slider-thumb]:rounded-full
    [&::-webkit-slider-thumb]:bg-white
    [&::-webkit-slider-thumb]:shadow
    [&::-webkit-slider-thumb]:focus-visible:ring
    [&::-webkit-slider-thumb]:focus-visible:ring-indigo-300
    [&::-moz-range-thumb]:h-5
    [&::-moz-range-thumb]:w-5                            
    [&::-moz-range-thumb]:rounded-full
    [&::-moz-range-thumb]:bg-white
    [&::-moz-range-thumb]:border-none
    [&::-moz-range-thumb]:shadow
    [&::-moz-range-thumb]:focus-visible:ring
    [&::-moz-range-thumb]:focus-visible:ring-indigo-300                            
" type="range" min="0" :max="prices.length - 1" :aria-valuetext="`${prices[value].contacts} contacts/month`" aria-label="Pricing Slider" x-model="value">
Enter fullscreen mode Exit fullscreen mode

Using the x-model directive, we've bound the slider value to the value property of the JavaScript object. This way, we can update the value property every time the slider is moved. Additionally, we've used :max to dynamically set the maximum value of the slider to prices.length - 1. Finally, we've used the :aria-valuetext directive to update the value of the aria-valuetext attribute every time the slider is moved. This way, the screen reader will read the the right number of contacts. Similarly, the paragraph above the slider will reflect the current value:

<div class="text-center text-sm text-slate-700 font-medium" x-text="`${prices[value].contacts} contacts/month`"></div>
Enter fullscreen mode Exit fullscreen mode

Next, to display the correct number of ticks and adjust the fill bar during slider interaction, we need to bring the values of the segmentsWidth and progress properties into the HTML as CSS variables. To do this, we'll use the :style directive:

<div class="max-w-sm mx-auto lg:max-w-3xl space-y-3 mb-12 lg:mb-16">
    <div class="text-center text-sm text-slate-700 font-medium" x-text="`${prices[value].contacts} contacts/month`"></div>
    <div class="relative flex items-center" :style="`--progress:${progress};--segments-width:${segmentsWidth}`">
        ...
Enter fullscreen mode Exit fullscreen mode

Now that we have set these two CSS variables, we can replace the hardcoded values in the previously defined custom classes:

<div class="
    absolute left-2.5 right-2.5 h-1.5 bg-slate-200 rounded-full overflow-hidden
    before:absolute
    before:inset-0
    before:bg-gradient-to-r
    before:from-indigo-300
    before:to-indigo-500
    before:[mask-image:_linear-gradient(to_right,theme(colors.white),theme(colors.white)_var(--progress),transparent_var(--progress))]
    after:absolute
    after:inset-0
    after:bg-[repeating-linear-gradient(to_right,transparent,transparent_calc(var(--segments-width)-1px),theme(colors.white/.7)_calc(var(--segments-width)-1px),theme(colors.white/.7)_calc(var(--segments-width)+1px))]
    [&[x-cloak]]:hidden
" aria-hidden="true" x-cloak></div>
Enter fullscreen mode Exit fullscreen mode

Finally, to complete the design, we need to show the value of each tick below the slider. To do this, we'll use a <ul> element and populate it with an x-for loop:

        ...
        <input class="
            relative appearance-none cursor-pointer w-full bg-transparent focus:outline-none
            [&::-webkit-slider-thumb]:appearance-none
            [&::-webkit-slider-thumb]:h-5
            [&::-webkit-slider-thumb]:w-5
            [&::-webkit-slider-thumb]:rounded-full
            [&::-webkit-slider-thumb]:bg-white
            [&::-webkit-slider-thumb]:shadow
            [&::-webkit-slider-thumb]:focus-visible:ring
            [&::-webkit-slider-thumb]:focus-visible:ring-indigo-300
            [&::-moz-range-thumb]:h-5
            [&::-moz-range-thumb]:w-5                            
            [&::-moz-range-thumb]:rounded-full
            [&::-moz-range-thumb]:bg-white
            [&::-moz-range-thumb]:border-none
            [&::-moz-range-thumb]:shadow
            [&::-moz-range-thumb]:focus-visible:ring
            [&::-moz-range-thumb]:focus-visible:ring-indigo-300                            
        " type="range" min="0" :max="prices.length - 1" :aria-valuetext="`${prices[value].contacts} contacts/month`" aria-label="Pricing Slider" x-model="value">
    </div>
    <div>
        <ul class="flex justify-between text-xs font-medium text-slate-500 px-2.5">
            <template x-for="(price, index) in prices" :key="index">
                <li class="relative"><span class="absolute -translate-x-1/2" x-text="price.contacts"></span></li>
            </template>
        </ul>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Updating prices on slider move

To complete our slider, we just need to update the prices based on the current slider value. All we have to do is add an x-text binding to the pricing tables element. Our component is ready to go! Here's the final code:

<div x-data="pricingSlider">

    <!-- Pricing slider -->
    <div class="max-w-sm mx-auto lg:max-w-3xl space-y-3 mb-12 lg:mb-16">
        <div class="text-center text-sm text-slate-700 font-medium" x-text="`${prices[value].contacts} contacts/month`"></div>
        <div class="relative flex items-center" :style="`--progress:${progress};--segments-width:${segmentsWidth}`">
            <div class="
                absolute left-2.5 right-2.5 h-1.5 bg-slate-200 rounded-full overflow-hidden
                before:absolute
                before:inset-0
                before:bg-gradient-to-r
                before:from-indigo-300
                before:to-indigo-500
                before:[mask-image:_linear-gradient(to_right,theme(colors.white),theme(colors.white)_var(--progress),transparent_var(--progress))]
                after:absolute
                after:inset-0
                after:bg-[repeating-linear-gradient(to_right,transparent,transparent_calc(var(--segments-width)-1px),theme(colors.white/.7)_calc(var(--segments-width)-1px),theme(colors.white/.7)_calc(var(--segments-width)+1px))]
                [&[x-cloak]]:hidden
            " aria-hidden="true" x-cloak></div>
            <input class="
                relative appearance-none cursor-pointer w-full bg-transparent focus:outline-none
                [&::-webkit-slider-thumb]:appearance-none
                [&::-webkit-slider-thumb]:h-5
                [&::-webkit-slider-thumb]:w-5
                [&::-webkit-slider-thumb]:rounded-full
                [&::-webkit-slider-thumb]:bg-white
                [&::-webkit-slider-thumb]:shadow
                [&::-webkit-slider-thumb]:focus-visible:ring
                [&::-webkit-slider-thumb]:focus-visible:ring-indigo-300
                [&::-moz-range-thumb]:h-5
                [&::-moz-range-thumb]:w-5                            
                [&::-moz-range-thumb]:rounded-full
                [&::-moz-range-thumb]:bg-white
                [&::-moz-range-thumb]:border-none
                [&::-moz-range-thumb]:shadow
                [&::-moz-range-thumb]:focus-visible:ring
                [&::-moz-range-thumb]:focus-visible:ring-indigo-300                            
            " type="range" min="0" :max="prices.length - 1" :aria-valuetext="`${prices[value].contacts} contacts/month`" aria-label="Pricing Slider" x-model="value">
        </div>
        <div>
            <ul class="flex justify-between text-xs font-medium text-slate-500 px-2.5">
                <template x-for="(price, index) in prices" :key="index">
                    <li class="relative"><span class="absolute -translate-x-1/2" x-text="price.contacts"></span></li>
                </template>
            </ul>
        </div>
    </div>

    <div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-2 items-start lg:max-w-[728px]">

        <!-- Pricing tab 1 -->
        <div class="h-full">                                
            <div class="relative flex flex-col h-full p-6 pb-10">
                <div class="h-8" aria-hidden="true"></div>
                <div class="mb-5">
                    <div class="text-slate-900 font-semibold mb-1">Starter</div>
                    <div class="inline-flex items-baseline mb-2">
                        <span class="text-slate-900 font-bold text-3xl">$</span>
                        <span class="text-slate-900 font-bold text-4xl" x-text="prices[value].plans.starter"></span>
                        <span class="text-slate-500 font-medium">/mo</span>
                    </div>
                    <div class="text-sm text-slate-500 mb-5">There are many variations available, but the majority have suffered.</div>
                    <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 transition-colors duration-150" href="#0">
                        Purchase Plan
                    </a>
                </div>
                <div class="text-slate-900 text-sm font-medium mb-4">Includes:</div>
                <ul class="text-slate-600 text-sm space-y-3 grow">
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Unlimited placeholder texts</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Consectetur adipiscing elit</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Excepteur sint occaecat cupidatat</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Officia deserunt mollit anim</span>
                    </li>
                </ul>
            </div>
        </div>

        <!-- Pricing tab 2 -->
        <div class="h-full">
            <div class="relative flex flex-col h-full p-6 pb-10 rounded-2xl bg-slate-900 border border-slate-900 shadow shadow-slate-950/5">
                <div class="absolute top-0 right-0 mr-6 -mt-4">
                    <div class="inline-flex items-center text-xs font-semibold py-1.5 px-3 bg-emerald-500 text-white rounded-full shadow-sm shadow-slate-950/5">Most Popular</div>
                </div>
                <div class="h-8" aria-hidden="true">
                    <svg class="drop-shadow-[0_0_8px_rgba(224,154,19,0.7)]" xmlns="http://www.w3.org/2000/svg" width="24" height="26">
                        <defs>
                            <linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%">
                                <stop offset="0%" stop-color="#FBBF24" />
                                <stop offset="100%" stop-color="#F59E0B" />
                            </linearGradient>
                        </defs>
                        <path fill="url(#a)" fill-rule="evenodd" d="M15 0 0 16h11L9 26l15-16H13z" />
                    </svg>
                </div>
                <div class="mb-5">
                    <div class="text-slate-200 font-semibold mb-1">Business</div>
                    <div class="inline-flex items-baseline mb-2">
                        <span class="text-slate-200 font-bold text-3xl">$</span>
                        <span class="text-slate-200 font-bold text-4xl" x-text="prices[value].plans.business"></span>
                        <span class="text-slate-500 font-medium">/mo</span>
                    </div>
                    <div class="text-sm text-slate-500 mb-5">There are many variations available, but the majority have suffered.</div>
                    <a class="w-full inline-flex justify-center whitespace-nowrap rounded-lg bg-indigo-500 px-3.5 py-2.5 text-sm font-medium text-white shadow-sm shadow-indigo-950/10 hover:bg-indigo-600 focus-visible:outline-none focus-visible:ring focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                        Purchase Plan
                    </a>
                </div>
                <div class="text-slate-200 text-sm font-medium mb-4">Everything in Starter, plus:</div>
                <ul class="text-slate-400 text-sm space-y-3 grow">
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Unlimited placeholder texts</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Consectetur adipiscing elit</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Excepteur sint occaecat cupidatat</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Officia deserunt mollit anim</span>
                    </li>
                    <li class="flex items-center">
                        <svg class="w-3 h-3 fill-emerald-500 mr-3 shrink-0" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
                            <path d="M10.28 2.28L3.989 8.575 1.695 6.28A1 1 0 00.28 7.695l3 3a1 1 0 001.414 0l7-7A1 1 0 0010.28 2.28z" />
                        </svg>
                        <span>Predefined chunks as necessary</span>
                    </li>
                </ul>
            </div>
        </div>

    </div>

</div>
<!-- Slider data and functionality: https://github.com/alpinejs/alpine -->
<script>
    document.addEventListener('alpine:init', () => {
        Alpine.data('pricingSlider', () => ({
            value: 2,
            prices: [
                {
                    contacts: '1K',
                    plans: {
                        starter: '5',
                        business: '9',
                    }
                },
                {
                    contacts: '5K',
                    plans: {
                        starter: '19',
                        business: '29',
                    }
                },
                {
                    contacts: '10K',
                    plans: {
                        starter: '29',
                        business: '49',
                    }
                },
                {
                    contacts: '15K',
                    plans: {
                        starter: '39',
                        business: '59',
                    }
                },
                {
                    contacts: '1M',
                    plans: {
                        starter: '1,490',
                        business: '2,490',
                    }
                },
            ],
            segmentsWidth: '100%',
            progress: '0%',
            segments: 1,
            calculateProgress() {
                this.segmentsWidth = 100 / this.segments + '%'
                this.progress = 100 / this.segments * this.value + '%'
            },
            init() {
                this.segments = this.prices.length - 1
                this.calculateProgress()
                this.$watch('value', () => this.calculateProgress())
            },
        }))
    })
</script>
Enter fullscreen mode Exit fullscreen mode

Conclusions

The component we've just created has a simple yet effective logic and, as you've seen, it performs really well. It's easy to customize and is designed to automatically adapt to a different number of steps. You can easily define your price tiers by adding or removing elements from the prices array. If you're wondering how to create a similar component with Next.js or Vue, we recommend taking a look at Gray - our Tailwind template specifically designed for startup landing pages.

Top comments (0)