Создание отзыва в виде всплывающей подсказки с помощью Tailwind и Alpine.js
Отзывы играют решающую роль в цифровом маркетинге, поскольку они служат социальным доказательством качества продукции и удовлетворенности клиентов. Однако распространенной проблемой с отзывами является то, что им часто не хватает визуальной привлекательности.
Отойдя от обыденности, мы создадим нетрадиционный компонент отзыва, который будет выглядеть оригинально, обеспечивая при этом хорошее взаимодействие с пользователем.
Создание HTML-структуры
Мы собираемся создать раздел, состоящий из текста и изображений клиентов. При наведении курсора мыши на изображение появится всплывающая подсказка с одобрительным сообщением. Для создания этого компонента мы будем использовать Tailwind CSS и Alpine.js.
Чередование текста с изображениями может показаться простым, но это сложнее, чем можно подумать, особенно когда речь идет о выравнивании элементов по вертикали без изменения высоты строки текста.
Чтобы сэкономить время и сосредоточиться на функциональности, мы подготовили базовую структуру HTML с помощью служебных классов Tailwind CSS:
<section class="text-center">
<div class="font-nycd text-xl text-indigo-500 mb-4">
<span class="relative inline-flex">
<span>Our promise</span>
<svg class="fill-indigo-500 absolute bottom-0" xmlns="http://www.w3.org/2000/svg" width="132" height="4">
<path fill-opacity=".4" fill-rule="evenodd" d="M131.014 2.344s-39.52 1.318-64.973 1.593c-25.456.24-65.013-.282-65.013-.282C-.34 3.623-.332 1.732.987 1.656c0 0 39.52-1.32 64.973-1.593 25.455-.24 65.012.282 65.012.282 1.356.184 1.37 1.86.042 1.999" />
</svg>
</span>
</div>
<div class="text-5xl leading-tight font-bold text-slate-900">
<span>We'll help you boost your revenues</span>
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1">
<button class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100">
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-01.jpg" width="52" height="52" alt="Testimonial 01">
</button>
</div>
<span>manage payrolls</span>
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1">
<button class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] -rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100">
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-02.jpg" width="52" height="52" alt="Testimonial 02">
</button>
</div>
<span>and save up to 50+ hours in duties every month</span>
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1">
<button class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100">
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-03.jpg" width="52" height="52" alt="Testimonial 03">
</button>
</div>
</div>
</section>
Теперь давайте добавим всплывающие подсказки:
<section class="text-center">
<div class="font-nycd text-xl text-indigo-500 mb-4">
<span class="relative inline-flex">
<span>Our promise</span>
<svg class="fill-indigo-500 absolute bottom-0" xmlns="http://www.w3.org/2000/svg" width="132" height="4">
<path fill-opacity=".4" fill-rule="evenodd" d="M131.014 2.344s-39.52 1.318-64.973 1.593c-25.456.24-65.013-.282-65.013-.282C-.34 3.623-.332 1.732.987 1.656c0 0 39.52-1.32 64.973-1.593 25.455-.24 65.012.282 65.012.282 1.356.184 1.37 1.86.042 1.999" />
</svg>
</span>
</div>
<div class="text-5xl leading-tight font-bold text-slate-900">
<span>We'll help you boost your revenues</span>
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-50">
<button class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100">
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-01.jpg" width="52" height="52" alt="Testimonial 01">
</button>
<div id="testimonial-01" role="tooltip" class="absolute top-full pt-5">
<div class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900">
<div class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3">
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
</div>
<span>manage payrolls</span>
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-40">
<button class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] -rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100">
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-02.jpg" width="52" height="52" alt="Testimonial 02">
</button>
<div id="testimonial-02" role="tooltip" class="absolute top-full pt-5">
<div class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900">
<div
class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3">
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
</div>
<span>and save up to 50+ hours in duties every month</span>
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-30">
<button class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100">
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-03.jpg" width="52" height="52" alt="Testimonial 03">
</button>
<div id="testimonial-03" role="tooltip" class="absolute top-full pt-5 [&[x-cloak]]:hidden">
<div class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900">
<div class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3">
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
</div>
</div>
</section>
В настоящее время все эти всплывающие подсказки видны – позже мы увидим, как скрыть их с помощью Alpine.js. Обратите внимание на стратегическое использование классов z-50
, z-40
и z-30
для управления порядком размещения и предотвращения скрытия всплывающих подсказок нижележащими изображениями.
Переключение видимости всплывающей подсказки
Теперь нам нужно интегрировать некоторую логику JavaScript для обработки видимости всплывающей подсказки, поэтому добавьте x-data
атрибут к элементу, содержащему изображение:
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-40" x-data="{ open: false }">
В рамках этой директивы мы определили open
свойство, изначально имеющее значение false
, указывающее, что всплывающая подсказка изначально скрыта.
Далее мы хотим, чтобы open
становился true
при наведении курсора на кнопку и false
при выходе курсора из родительского элемента. Для этого мы добавим @mouseover
событие к кнопке и @mouseover.outside
к ее оболочке:
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-50" x-data="{ open: false }" @mouseover.outside="open = false">
<button class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100" @mouseover="open = true">
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-01.jpg" width="52" height="52" alt="Testimonial 01">
</button> ...
Наконец, мы применим Alpine.js утилиты перехода для эффекта плавного перехода:
<div
id="testimonial-01"
role="tooltip"
class="absolute top-full pt-5 [&[x-cloak]]:hidden"
x-cloak
>
<div
class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900"
x-show="open"
x-transition:enter="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-200 delay-100"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-100 delay-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3">
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
Отлично! Теперь всплывающая подсказка скрыта по умолчанию и исчезает – с едва заметным вертикальным перемещением - при наведении курсора мыши на изображение.
Также обратите внимание, что мы использовали атрибут x-cloak
, чтобы предотвратить кратковременное отображение всплывающей подсказки перед полной загрузкой Alpine.js.
Управление навигацией с клавиатуры
Когда дело дойдет до реализации компонента, мы позаботимся о том, чтобы по контенту можно было легко перемещаться с помощью клавиатуры, просто нажав клавишу Tab
.
Итак, давайте завершим интеграцию button
добавлением focus
события:
<button
class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100"
@mouseover="open = true"
@focus="open = true"
>
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-01.jpg" width="52" height="52" alt="Testimonial 01">
</button>
Теперь всплывающая подсказка становится видимой, когда button
также получает фокус. Однако нам все равно нужно закрыть всплывающую подсказку, когда элемент контейнера теряет фокус:
<div
class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-50"
x-data="{ open: false }"
@mouseover.outside="open = false"
@focusout="await $nextTick();!$el.contains($focus.focused()) && (open = false)"
>
<button
class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100"
@mouseover="open = true"
@focus="open = true"
> ...
На этом этапе вам может быть интересно, почему мы просто не использовали выражение open = false
для @mouseover
события. Что ж, если внутри всплывающей подсказки есть ссылки или другие фокусируемые элементы, мы не должны ее закрывать! Вот почему мы использовали плагин фокусировки из Alpine.js чтобы определить, находится ли сфокусированный элемент во всплывающей подсказке. Если это не так, мы можем закрыть всплывающую подсказку.
Предотвращение переполнения всплывающей подсказки
Теперь давайте убедимся, что всплывающая подсказка не переполняет область просмотра, особенно при различных размерах экрана. В случаях, когда всплывающая подсказка выходит за пределы экрана, особенно на дисплеях меньшего размера, мы будем использовать несколько строк JavaScript для динамической настройки ее расположения.
Для начала назначьте x-ref="tooltip"
контейнеру всплывающей подсказки и добавьте x-init
директиву к элементу, определяющему цвет фона:
<div
id="testimonial-01"
role="tooltip"
class="absolute top-full pt-5 [&[x-cloak]]:hidden"
x-ref="tooltip"
x-cloak
>
<div
class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900"
x-show="open"
x-transition:enter="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-200 delay-100"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-100 delay-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div
class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3"
x-init="$watch('open', value => { $nextTick(() => {
$refs.tooltip.getBoundingClientRect().left < 0 ? $el.style.left = Math.abs($refs.tooltip.getBoundingClientRect().left) + $root.getBoundingClientRect().left - 4 + 'px' : $el.style.left = null;
$refs.tooltip.getBoundingClientRect().right > document.documentElement.offsetWidth ? $el.style.right = Math.abs($refs.tooltip.getBoundingClientRect().right) - $root.getBoundingClientRect().right - 4 + 'px' : $el.style.right = null;
} )} )"
>
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
В приведенном выше коде $watch
метод из Alpine.js отслеживает изменения в open
свойстве. Когда он обнаруживает изменение, он запускает функцию, которая корректирует положение всплывающей подсказки:
- Если всплывающая подсказка заполняет экран слева, она перемещается вправо.
- Если всплывающая подсказка заполняет экран вправо, она перемещается влево.
Это дополнение гарантирует, что всплывающая подсказка останется в пределах экрана.
Меньшая непрозрачность взаимодействия
Пока что компонент идеально функционален и доступен, но давайте добавим небольшое улучшение. Когда пользователь наводит курсор на изображение, мы хотим уменьшить непрозрачность всех остальных элементов. Для этого мы назначим класс, привязанный к open
свойству:
<div
class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-50"
x-data="{ open: false }"
:class="{ 'active': open }"
@mouseover.outside="open = false"
@focusout="await $nextTick();!$el.contains($focus.focused()) && (open = false)"
> ...
По сути, когда open
есть true
, active
класс добавляется к элементу контейнера. Теперь мы можем использовать этот класс, чтобы снизить непрозрачность родственных элементов.
Для тех, кто не знаком с CSS, комбинатор последующих дочерних элементов (~
) может выбирать элементы, которые являются дочерними элементами, встречающимися после определенного элемента. Итак, чтобы снизить непрозрачность элементов последующих типов, мы можем использовать пользовательский класс [&.active~*]:opacity-25
для всех элементов, содержащих текст и изображения.
Теперь, чтобы решить проблему снижения непрозрачности для предыдущих элементов, мы можем использовать ~
комбинатор в сочетании с :has()
псевдоклассом. Это гарантирует, что все предыдущие элементы выбраны. Результирующий класс [&:has(~.active)]:opacity-25
.
Наконец, добавьте классы transition-opacity
и duration-200
для плавного эффекта перехода непрозрачности.
С учетом только что внесенных изменений наш компонент теперь завершен:
<section class="text-center">
<div class="font-nycd text-xl text-indigo-500 mb-4">
<span class="relative inline-flex">
<span>Our promise</span>
<svg class="fill-indigo-500 absolute bottom-0" xmlns="http://www.w3.org/2000/svg" width="132" height="4">
<path fill-opacity=".4" fill-rule="evenodd" d="M131.014 2.344s-39.52 1.318-64.973 1.593c-25.456.24-65.013-.282-65.013-.282C-.34 3.623-.332 1.732.987 1.656c0 0 39.52-1.32 64.973-1.593 25.455-.24 65.012.282 65.012.282 1.356.184 1.37 1.86.042 1.999" />
</svg>
</span>
</div>
<div class="text-5xl leading-tight font-bold text-slate-900">
<span class="[&:has(~.active)]:opacity-25 [&.active~*]:opacity-25 transition-opacity duration-200">We'll help you boost your revenues</span>
<div
class="[&:has(~.active)]:opacity-25 [&.active~*]:opacity-25 transition-opacity duration-200 relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-50"
x-data="{ open: false }"
:class="{ 'active': open }"
@mouseover.outside="open = false"
@focusout="await $nextTick();!$el.contains($focus.focused()) && (open = false)"
>
<button
class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100"
:class="{ 'rotate-0': open }"
aria-labelledby="testimonial-01"
@mouseover="open = true"
@focus="open = true"
>
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-01.jpg" width="52" height="52" alt="Testimonial 01">
</button>
<div
id="testimonial-01"
role="tooltip"
class="absolute top-full pt-5 [&[x-cloak]]:hidden"
x-ref="tooltip"
x-cloak
>
<div
class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900"
x-show="open"
x-transition:enter="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-200 delay-100"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-100 delay-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div
class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3"
x-init="$watch('open', value => { $nextTick(() => {
$refs.tooltip.getBoundingClientRect().left < 0 ? $el.style.left = Math.abs($refs.tooltip.getBoundingClientRect().left) + $root.getBoundingClientRect().left - 4 + 'px' : $el.style.left = null;
$refs.tooltip.getBoundingClientRect().right > document.documentElement.offsetWidth ? $el.style.right = Math.abs($refs.tooltip.getBoundingClientRect().right) - $root.getBoundingClientRect().right - 4 + 'px' : $el.style.right = null;
} )} )"
>
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
</div>
<span class="[&:has(~.active)]:opacity-25 [&.active~*]:opacity-25 transition-opacity duration-200">manage payrolls</span>
<div
class="[&:has(~.active)]:opacity-25 [&.active~*]:opacity-25 transition-opacity duration-200 relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-40"
x-data="{ open: false }"
:class="{ 'active': open }"
@mouseover.outside="open = false"
@focusout="await $nextTick();!$el.contains($focus.focused()) && (open = false)"
>
<button
class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] -rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100"
:class="{ 'rotate-0': open }"
aria-labelledby="testimonial-02"
@mouseover="open = true"
@focus="open = true"
>
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-02.jpg" width="52" height="52" alt="Testimonial 02">
</button>
<div
id="testimonial-02"
role="tooltip"
class="absolute top-full pt-5 [&[x-cloak]]:hidden"
x-ref="tooltip"
x-cloak
>
<div
class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900"
x-show="open"
x-transition:enter="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-200 delay-100"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-100 delay-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div
class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3"
x-init="$watch('open', value => { $nextTick(() => {
$refs.tooltip.getBoundingClientRect().left < 0 ? $el.style.left = Math.abs($refs.tooltip.getBoundingClientRect().left) + $root.getBoundingClientRect().left - 4 + 'px' : $el.style.left = null;
$refs.tooltip.getBoundingClientRect().right > document.documentElement.offsetWidth ? $el.style.right = Math.abs($refs.tooltip.getBoundingClientRect().right) - $root.getBoundingClientRect().right - 4 + 'px' : $el.style.right = null;
} )} )"
>
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
</div>
<span class="[&:has(~.active)]:opacity-25 [&.active~*]:opacity-25 transition-opacity duration-200">and save up to 50+ hours in duties every month</span>
<div
class="[&:has(~.active)]:opacity-25 [&.active~*]:opacity-25 transition-opacity duration-200 relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-30"
x-data="{ open: false }"
:class="{ 'active': open }"
@mouseover.outside="open = false"
@focusout="await $nextTick();!$el.contains($focus.focused()) && (open = false)"
>
<button
class="h-full w-full focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 rounded-[20px] rotate-[4deg] transition duration-200 ease-[cubic-bezier(.5,.85,.25,1.8)] delay-100"
:class="{ 'rotate-0': open }"
aria-labelledby="testimonial-03"
@mouseover="open = true"
@focus="open = true"
>
<img class="absolute top-1/2 -translate-y-1/2 rounded-[inherit]" src="./testimonial-03.jpg" width="52" height="52" alt="Testimonial 03">
</button>
<div
id="testimonial-03"
role="tooltip"
class="absolute top-full pt-5 [&[x-cloak]]:hidden"
x-ref="tooltip"
x-cloak
>
<div
class="relative w-80 after:absolute after:-top-1.5 after:left-1/2 after:-translate-x-1/2 after:h-3 after:w-3 after:rounded-tl-sm after:rotate-45 after:bg-slate-900"
x-show="open"
x-transition:enter="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-200 delay-100"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-[cubic-bezier(.5,.85,.25,1.8)] duration-100 delay-100"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div
class="relative bg-slate-900 p-5 rounded-3xl shadow-xl text-left text-sm text-slate-200 font-medium space-y-3"
x-init="$watch('open', value => { $nextTick(() => {
$refs.tooltip.getBoundingClientRect().left < 0 ? $el.style.left = Math.abs($refs.tooltip.getBoundingClientRect().left) + $root.getBoundingClientRect().left - 4 + 'px' : $el.style.left = null;
$refs.tooltip.getBoundingClientRect().right > document.documentElement.offsetWidth ? $el.style.right = Math.abs($refs.tooltip.getBoundingClientRect().right) - $root.getBoundingClientRect().right - 4 + 'px' : $el.style.right = null;
} )} )"
>
<svg class="fill-indigo-500" xmlns="http://www.w3.org/2000/svg" width="17" height="14" aria-hidden="true">
<path fill-rule="nonzero" d="M2.014 3.68c.276-1.267.82-2.198 1.629-2.79C4.453.295 5.627 0 7.167 0c.514 0 .908.02 1.185.061L5.035 10.49c-.75 2.494-2.429 3.66-5.035 3.496L2.014 3.68Zm8.648 0c.237-1.227.77-2.147 1.6-2.76C13.09.307 14.274 0 15.814 0c.514 0 .909.02 1.185.061L13.683 10.49c-.79 2.494-2.468 3.66-5.035 3.496L10.662 3.68Z" />
</svg>
<p>
This component is AWESOME. The hover feature is well-thought-out. Even the smaller details, like using colors, really helps everything stay organized. Cruip is amazing and I really enjoy using it.
</p>
<p>
Mary Smith <span class="text-slate-600">-</span> <span class="text-slate-400">Software Engineer</span>
</p>
</div>
</div>
</div>
</div>
</div>
</section>
Выводы
Этот урок - еще одна демонстрация того, насколько мощным и универсальным является сочетание Tailwind CSS + Alpine.js. Всего несколькими строками кода – и все это в HTML-документе! – мы создали интерактивный, доступный и отзывчивый компонент.