Creating a Tooltip-like Testimonial with Tailwind and Alpine.js
Cruip
Posted on December 12, 2023
Live Demo / Download
Testimonials play a crucial role in digital marketing as they serve as social proof of product quality and customer satisfaction. However, a common issue with testimonials is that they often lack visual appeal. In a previous tutorial, we showed how to build a fancy testimonial slider using Tailwind CSS; now, we are doubling down, drawing inspiration from the cool shot by the Significa team. Breaking away from the ordinary, we will build an unconventional testimonial component that looks original while ensuring a good user experience.
Creating the HTML structure
We are going to create a section made of text and images of clients. On hovering over the image, a tooltip will appear with the endorsement message. To create this component, we will use Tailwind CSS and Alpine.js. Alternating text with images may seem simple, but it's trickier than one may think, especially when it comes to align the elements vertically without affecting the line-height of the text. To save time and focus on the functionality, we've prepared the basic HTML structure with the Tailwind CSS utility classes:
<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>
Now, let's add the tooltips:
<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>
Currently, these tooltips are all visible - we will see later how to hide them with Alpine.js. Notice the strategic use of z-50
, z-40
, and z-30
classes to control the stacking order and prevent tooltips from being covered by underlying images.
Toggling tooltip visibility
Now, we need to integrate some JavaScript logic to handle tooltip visibility, so add an x-data
attribute to the element containing the image:
<div class="relative inline-flex justify-center w-[52px] h-[52px] align-middle -translate-y-1 z-40" x-data="{ open: false }">
Within this directive, we have defined a open
property initially set to false
, indicating that the tooltip is initially hidden. Next, we want open
to become true
on hovering over the button, and false
when the cursor exits the parent element. To do this, we'll add a @mouseover
event to the button and @mouseover.outside
to its wrapper:
<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> ...
Lastly, we'll apply Alpine.js transition utilities for the fade-in/fade-out effect:
<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>
Great! Now, the tooltip is hidden by default and fades in - with a subtle vertical translation - when you hover over the image. Also, notice that we have used the x-cloak
attribute to prevent the tooltip from briefly appearing before Alpine.js is fully loaded.
Handling keyboard navigation
If you've been following our previous tutorials, you know how important accessibility is to us. That's why, when it comes to implementing this component, we'll make sure that the content can be easily navigated using the keyboard by simply pressing the Tab
key. So, let's complete the integration of the button
with the addition of a focus
event:
<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>
Now, the tooltip becomes visible when the button
receives focus too. However, we still need to close the tooltip when the container element loses focus:
<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"
> ...
At this point, you might be wondering why we didn't just use the expression open = false
for the @mouseover
event . Well, if there are links or other focusable elements inside the tooltip, we shouldn't close it! That's why we utilized the focus plugin of Alpine.js to determine if the focused element is within the tooltip. If it's not, then we can close the tooltip.
Prevent tooltip overflow
Now, let's ensure the tooltip doesn't overflow the viewport, especially on varying screen sizes. In cases where the tooltip goes outside the screen, particularly on smaller displays, we'll use a few lines of JavaScript to dynamically adjust its positioning. To start, assign an x-ref="tooltip"
to the tooltip's container and add an x-init
directive to the element defining the background color:
<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>
In the code above, the $watch
method from Alpine.js monitors changes in the open
property. When it detects a change, it triggers a function that adjusts the tooltip's position:
- If the toolptip overflows the screen to the left, it moves the tooltip right.
- If the toolptip overflows the screen to the right, it moves the tooltip left.
This addition ensures the tooltip remains within the screen boundaries.
Lower sibling opacity on interaction
So far, the component is perfectly functional and accessible, but let's add a subtle enhancement. When a user hovers over an image, we want to decrease the opacity of all other elements. To do this, we'll assign a class bound to the open
property:
<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)"
> ...
In essence, when open
is true
, the active
class is added to the container element. Now, we can use this class to lower the opacity of sibling elements. For those unfamiliar with CSS, the subsequent-sibling combinator (~
) can select elements that are siblings occurring after a specific element. So, to lower the opacity of subsequent-sibling elements, we can use the custom class [&.active~*]:opacity-25
on all elements containing text and images. Now, to address the challenge of lowering opacity for preceding elements, we can use the ~
combinator in conjunction with the :has()
pseudo-class. This ensures all preceding elements are selected, as explained by Tobias Ahlin Bjerrome. The resulting class is [&:has(~.active)]:opacity-25
. Finally, add the transition-opacity
and duration-200
classes for a smooth opacity transition effect. With the changes just made, our component is now complete:
<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>
Conclusions
This tutorial is yet another demonstration of how powerful and versatile the Tailwind CSS + Alpine.js combo is. With just a few lines of code - all within the HTML document! - we have created an interactive, accessible, and responsive component. If you've found this tutorial useful, we recommend checking out our HTML templates built with Tailwind, all designed with Alpine.js. Feel free to experiment further, customize the component to suit your needs, and explore additional features that Tailwind CSS and Alpine.js have to offer. Happy coding!
Posted on December 12, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.