DevGang
Авторизоваться

Создание таблицы цен с ползунком диапазона с использованием Tailwind CSS и Alpine.js

В мире SaaS продукты или услуги часто предлагаются по фиксированной цене вместе со скидкой на годовую подписку. Этот традиционный подход широко используется и демонстрируется в этой галерее примеров лучших страниц с ценами.

Однако бывают ситуации, когда стоимость зависит от количества. Это известно как «tiered pricing» и обычно используется в хостинге, хранилище, API, SMS и других услугах. Примером этого является Brevo, где тарифные планы меняются в зависимости от количества электронных писем, отправляемых каждый месяц. Такие структуры ценообразования обычно представляются с помощью ползунка диапазона, позволяющего пользователям регулировать количество и видеть соответствующую цену.

Цель этого руководства — создать несколько таблиц цен с ползунком диапазона на основе примера Brevo и с использованием Tailwind CSS и Alpine.js. Как всегда, мы уделяем максимальное внимание требованиям доступности, чтобы наша таблица цен была удобной для всех.

Создание HTML

Чтобы структурировать наш компонент, мы будем повторно использовать таблицы цен, созданные в предыдущем руководстве. Таким образом, мы можем сосредоточиться на разработке ползунка диапазона и его взаимодействии с отображаемыми ценами.

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

В приведенном выше коде у нас есть две таблицы цен: одна для плана «Starter» и визуально выделенная в темной теме для плана «Business». Чтобы сэкономить время, мы предварительно разработали для вас стили этих компонентов.

Теперь давайте посмотрим, как интегрировать ползунок диапазона и сделать его функциональным.

Создание ползунка диапазона

Прежде чем создавать этот компонент, я потратил некоторое время на то, чтобы подумать, использовать ли встроенный диапазон ввода или собственный ползунок с атрибутами ARIA. В итоге я выбрал первый вариант. Хотя стилизация ввода диапазона может быть сложной задачей, это проще, чем создавать слайдер с нуля, особенно когда речь идет о том, чтобы сделать его полностью функциональным с помощью JavaScript.

Более того, мы решили использовать Alpine.js, потому что предпочитаем быстрые решения, а не углубляемся в сложности JavaScript. К концу этого урока вы увидите, что работать со всей логикой оказалось проще, чем стилизовать слайдер!

Начнем с добавления элемента ввода на страницу:

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

На данный момент у слайдера нет стилей. В разных браузерах он выглядит по-разному, поскольку является собственным элементом HTML. Цель этого руководства — придать слайдеру индивидуальный и единообразный стиль во всех браузерах.

Стилизация ползунка

Прежде чем продолжить, давайте внимательно изучим дизайн слайдера и выработаем стратегию нашего подхода:

Ползунок слайдера представляет собой белый круг с тонкой тенью размером 20×20 пикселей. Мы применим соответствующий стиль к псевдоэлементу, который его представляет — ::-webkit-slider-thumb в браузерах на основе Webkit (Chrome, Safari, Edge и т. д.) и ::-moz-range-thumb в Firefox. Давайте наметим классы Tailwind, которые будут добавлены к элементу ввода:

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

Тем, кто предпочитает традиционный подход «семантического CSS», код на этом этапе может показаться несколько обширным. Это следствие использования служебных классов. Если множество классов в одном элементе кажется вам подавляющим, вы всегда можете создать собственный класс и применить его к входному элементу.

Обратите внимание, что мы также уже задали стиль фокуса. Итак, когда вы используете клавишу Tab, вы можете легко перейти к ползунку и отрегулировать значение, используя стрелки вправо и влево. Когда элемент окажется в фокусе, вы увидите красивое кольцо фокусировки цвета индиго, которое соответствует цвету фона ползунка.

Стилизация слайдера

Давайте вернемся к нашему первоначальному дизайну и теперь сосредоточимся на слайдере: это горизонтальная полоса с закругленными краями, высотой 6 пикселей и очень светло-серого цвета. С этим элементом есть две основные сложности:

  • Область слева от большого пальца представляет собой линейный градиент, начинающийся со светлого индиго и заканчивающийся чуть более темным индиго.
  • Весь трек отмечен галочкой на каждом этапе.

Это настоящая проблема CSS. Мы не можем использовать псевдоэлементы ::-webkit-slider-runnable-track и ::-moz-range-track, потому что один элемент не позволяет нам включить все детали дизайна.

Итак, давайте сделаем нативный трек прозрачным и добавим новый HTML-элемент в качестве замены:

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

Как видите, мы добавили <div> с атрибутом aria-hidden="true", который скрывает его от программ чтения с экрана.

Далее мы будем использовать псевдоэлемент ::before для создания линейного градиента и псевдоэлемент ::after для тиков. Начнем с линейного градиента:

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

Полоса заполнения охватывает всю полосу и идеально обрезается пополам с помощью маски — позже мы увидим, как динамически регулировать положение маски на основе значения ползунка.

Теперь давайте воспользуемся псевдоэлементом ::after, чтобы отобразить тики:

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

Мы используем повторяющийся линейный градиент для тиков. Шагов ползунка 5, поэтому полоска состоит из 4 сегментов, каждый из которых занимает 25% общей длины.

Чтобы создать повторяющийся шаблон градиента для меток общей шириной 2 пикселя, мы воспользуемся следующим подходом:

  • От 0% до 25%-1 цвет остается прозрачным.
  • От 25% - 1 до 25%+1 цвет белый с непрозрачностью 70%.

Таким образом, у нас будет 3 тика на 25%, 50% и 75% длины бара. Круто, не так ли?

Как очевидно, значение 25% работает, пока у нас есть диапазон в 5 шагов. Позже мы увидим, как сделать это значение динамическим.

Добавление логики Alpine.js

До сих пор мы стилизовали ползунок диапазона. Теперь давайте добавим Alpine.js, чтобы добавить интерактивности. Сначала подключите библиотеку в заголовок документа:

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

Следующим шагом является добавление атрибута x-data к элементу, который содержит как входные данные диапазона, так и таблицы цен:

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

Теперь сразу после элемента x-data="pricingSlider"> добавим тег <script>, содержащий объект JavaScript с логикой:

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

В этом коде происходит несколько вещей, поэтому давайте разберем его:

  • Свойство value представляет текущее значение ползунка, инициализированное значением 2.
  • Свойство цены содержит массив объектов, каждый из которых представляет сегмент ползунка, с соответствующими контактными номерами и ценами для планов «Стартовый» и «Бизнес».
  • Свойство elementsWidth обозначает ширину каждого ползунка, инициализированную значением 100%.
  • Свойство Progress представляет ширину полосы заполнения, инициализированную значением 0%.
  • Метод CalculationProgress() вычисляет ширину каждого сегмента (segmentsWidth) и ширину полосы заполнения (прогресс).
  • Метод init(), выполняемый при запуске, вызывает метод CalculationProgress(). Кроме того, к свойству value добавляется наблюдатель, так что каждый раз, когда значение ползунка изменяется, метод CalculationProgress() вызывается снова.

Завершение функциональности слайдера

Теперь у нас есть все необходимое, чтобы слайдер работал. Давайте привяжем значение ползунка:

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

Используя директиву x-model, мы привязали значение ползунка к свойству value объекта JavaScript. Таким образом, мы можем обновлять свойство value каждый раз при перемещении ползунка.

Кроме того, мы использовали :max для динамической установки максимального значения ползунка Price.length - 1.

Наконец, мы использовали директиву :aria-valuetext для обновления значения атрибута aria-valuetext каждый раз при перемещении ползунка. Таким образом, программа чтения с экрана прочитает нужное количество контактов. Аналогично, абзац над ползунком будет отражать текущее значение:

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

Далее, чтобы отобразить правильное количество тиков и настроить полосу заполнения во время взаимодействия с ползунком, нам нужно перенести значения свойств сегментов Width и Progress в HTML в виде переменных CSS. Для этого мы воспользуемся директивой :style:

<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}`">
        ...

Теперь, когда мы установили эти две переменные CSS, мы можем заменить жестко запрограммированные значения в ранее определенных пользовательских классах:

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

Наконец, чтобы завершить дизайн, нам нужно показать значение каждого тика под ползунком. Для этого мы воспользуемся элементом <ul> и заполним его циклом x-for:

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

Обновление цен при движении ползунка

Чтобы завершить наш слайдер, нам просто нужно обновить цены на основе текущего значения ползунка. Все, что нам нужно сделать, это добавить привязку x-text к элементу таблиц цен. Наш компонент готов к работе! Вот окончательный код:

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

Заключение

Компонент, который мы только что создали, имеет простую, но эффективную логику и, как вы видели, работает очень хорошо. Его легко настроить, и он предназначен для автоматической адаптации к разному количеству шагов. Вы можете легко определить свои ценовые категории, добавляя или удаляя элементы из массива prices.

Если вам интересно, как создать аналогичный компонент с помощью Next.js или Vue, мы рекомендуем взглянуть на Gray — наш шаблон Tailwind, специально разработанный для целевых страниц стартапа.

Источник:

#JavaScript #CSS #HTML #Alpine.js
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу