✨ Get all templates and new upcoming releases for just $89. Limited time offer ✨

Make a Stunning Card Hover Animations with Tailwind CSS

Preview of the card animations we're going to build in this tutorial

In this tutorial, we will show you a simple technique to make your landing page’s feature cards more appealing by adding a hover animation. It’s super easy to do but the results are amazing!

Basically, we’ll create two images for each card. When someone hovers over the card, the second image will smoothly replace the first one with a crossfade effect and a subtle upward transition. Let’s get started!

Building the HTML structure

The demo we we are going to create consists of 3 cards arranged in a row using a CSS grid. Here is the structure for hosting the cards:

<section class="grid md:grid-cols-3 gap-6 max-md:max-w-xs mx-auto">
    
    <!-- Card #1 -->
    <!-- Card #2 -->
    <!-- Card #3 -->

</section>

On smaller screens we have a single column layout, whose maximum width is 320px (max-md:max-w-xs). By using the class md:grid-cols-3, we are enabling the 3-column layout starting from the breakpoint defined by the md: prefix in Tailwind (i.e. 728px).

Creating the cards

All cards will have the same structure, with only the text and image changing. Here is the HTML for a single card:

<div class="[background:linear-gradient(theme(colors.slate.900),theme(colors.slate.900))_padding-box,linear-gradient(45deg,theme(colors.slate.800),theme(colors.slate.600/.8),theme(colors.slate.800))_border-box] relative before:absolute before:inset-0 before:bg-[url('./noise.png')] before:bg-[length:352px_382px] rounded-2xl border border-transparent">
    <div class="relative">
        <div class="px-6 py-5">
            <div class="font-nycd text-lg text-indigo-500 mb-1">Label</div>
            <div class="text-lg font-bold text-slate-200 mb-1">Daily Reports</div>
            <p class="text-sm text-slate-500">Building truly great products is both art and science. It's part intuition and part data.</p>
        </div>
        <div>
            <img src="./card-01.png" width="350" height="240" alt="Card image 01">
        </div>
    </div>
</div>

The code above gives us a card with a gradient border and a background that adds some grainy texture. Inside the card, we have some text and the image that we will animate when the user hovers over the card.

Overlaying the second image

Now, to achieve the animation effect, we need two images: one visible by default (card-01.png) and another displayed on hover (card-01-hover.png). The former is already in place; now let’s add the latter:

<div class="[background:linear-gradient(theme(colors.slate.900),theme(colors.slate.900))_padding-box,linear-gradient(45deg,theme(colors.slate.800),theme(colors.slate.600/.8),theme(colors.slate.800))_border-box] relative before:absolute before:inset-0 before:bg-[url('./noise.png')] before:bg-[length:352px_382px] rounded-2xl border border-transparent">
    <div class="relative">
        <div class="px-6 py-5">
            <div class="font-nycd text-lg text-indigo-500 mb-1">Label</div>
            <div class="text-lg font-bold text-slate-200 mb-1">Daily Reports</div>
            <p class="text-sm text-slate-500">Building truly great products is both art and science. It's part intuition and part data.</p>
        </div>
        <div class="relative">
            <img src="./card-01.png" width="350" height="240" alt="Card image 01">
            <img class="absolute top-0 left-0 opacity-0" src="./card-01-hover.png" width="350" height="240" alt="Card image 01 displaying on hover" aria-hidden="true">
        </div>
    </div>
</div>

To overlay the two images, we must position the second image absolutely relative to the first. The second image is initially invisible (opacity-0) and marked as not visible to screen readers (aria-hidden="true").

Creating the transition effect

The final step is defining the transition that occurs on card hover. We’ll use the group-hover modifier from Tailwind for this purpose:

<div class="group [background:linear-gradient(theme(colors.slate.900),theme(colors.slate.900))_padding-box,linear-gradient(45deg,theme(colors.slate.800),theme(colors.slate.600/.8),theme(colors.slate.800))_border-box] relative before:absolute before:inset-0 before:bg-[url('./noise.png')] before:bg-[length:352px_382px] rounded-2xl border border-transparent">
    <div class="relative">
        <div class="px-6 py-5">
            <div class="font-nycd text-lg text-indigo-500 mb-1">Label</div>
            <div class="text-lg font-bold text-slate-200 mb-1">Daily Reports</div>
            <p class="text-sm text-slate-500">Building truly great products is both art and science. It's part intuition and part data.</p>
        </div>
        <div class="relative group-hover:-translate-y-1 transition-transform duration-500 ease-in-out">
            <img class="group-hover:opacity-0 transition-opacity duration-500" src="./card-01.png" width="350" height="240" alt="Card image 01">
            <img class="absolute top-0 left-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500" src="./card-01-hover.png" width="350" height="240" alt="Card image 01 displaying on hover" aria-hidden="true">
        </div>
    </div>
</div>

In the code above, we added the group class to the card div. This allows us to use the group-hover prefix to hide the initial image on card hover and simultaneously reveal the second image.

Then, to achieve a crossfade effect, we added the transition-opacity class to both images, and the duration-500 class to set a duration of 0.5 seconds.

Finally, to make it even better, we added the group-hover:-translate-y-1 class to the div that holds the images. This class moves the image 4px up when the card is hovered over. Of course, we also needed to add some transition effects for this to work smoothly. So, we added the classes transition-transform, duration-500, and ease-in-out.

Now we can put it all together:

<section class="text-center">
    <div class="font-nycd text-2xl text-indigo-500 subpixel-antialiased 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

Implementing micro-interactions, such as the one demonstrated in this tutorial, adds interest and engagement to your landing page. The key to success lies in moderation — avoid overdoing effects and maintain a clean, minimalist approach.

We hope you’ve enjoyed this tutorial. If you have any questions of feedback, don’t hesitate to get in touch with us!