Создание таблицы цен с ползунком диапазона с использованием 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, специально разработанный для целевых страниц стартапа.