Originally published at cruip.com
--
Lately, weâve been seeing a lot more animations being used on landing pages. Itâs not just about the product anymore; how you present it also shows off that youâre professional and pay attention to the little details.
When used in the right way, animation effects can really help engaging users. Itâs all about finding the right balance: too many visuals can overwhelm people, but the right amount can guide them towards taking action. Our curated gallery of landing page designs showcases many examples of cool animations. One of our favorites is BlackWallet. Weâve taken inspiration to their landing to create a cool stacking cards effect that reveals as you scroll.
Weâll be using Tailwind CSS for styling, and for the animation weâll be using Alpine.js and the Intersect Plugin.
Letâs get into the technical stuff!
Creating the HTML structure
To start off, letâs create the HTML structure. I wonât go into too much detail about this since itâs not the main focus of this tutorial. Iâll just give you the HTML code with all the necessary Tailwind utility classes for styling:
<div class="max-w-5xl mx-auto">
<div class="relative z-0 space-y-14">
<!-- Section #1 -->
<section>
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out">
<div class="md:flex justify-between items-center">
<div class="shrink-0 px-12 py-14 max-md:pb-0 md:pr-0">
<div class="md:max-w-md">
<div class="font-nycd text-xl text-indigo-500 mb-2 relative inline-flex justify-center items-end">
Interesting
<svg class="absolute fill-indigo-500 opacity-40 -z-10" xmlns="http://www.w3.org/2000/svg" width="88" height="4" viewBox="0 0 88 4" aria-hidden="true" preserveAspectRatio="none">
<path d="M87.343 2.344S60.996 3.662 44.027 3.937C27.057 4.177.686 3.655.686 3.655c-.913-.032-.907-1.923-.028-1.999 0 0 26.346-1.32 43.315-1.593 16.97-.24 43.342.282 43.342.282.904.184.913 1.86.028 1.999" />
</svg>
</div>
<h1 class="text-4xl font-extrabold text-slate-50 mb-4">The modern way to find high-quality devs</h1>
<p class="text-slate-400 mb-6">We're the world's largest marketplace of quality developers for early-stage startups. Need a hand with development? Grab one of ours!</p>
<a class="text-sm font-medium inline-flex items-center justify-center px-3 py-1.5 border border-slate-700 rounded-lg tracking-normal transition text-slate-300 hover:text-slate-50 group" href="#0">
Learn More <span class="text-slate-600 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">-></span>
</a>
</div>
</div>
<img class="mx-auto max-md:-translate-x-[5%]" src="./illustration-01.png" width="519" height="490" alt="Illustration 01">
</div>
<div class="absolute left-12 bottom-0 h-14 flex items-center text-xs font-medium text-slate-400">01</div>
</div>
</section>
<!-- Section #2 -->
<section>
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out">
<div class="md:flex justify-between items-center">
<div class="shrink-0 px-12 py-14 max-md:pb-0 md:pr-0">
<div class="md:max-w-md">
<div class="font-nycd text-xl text-sky-500 mb-2 relative inline-flex justify-center items-end">
Engaging
<svg class="absolute fill-sky-500 opacity-40 -z-10" xmlns="http://www.w3.org/2000/svg" width="88" height="4" viewBox="0 0 88 4" aria-hidden="true" preserveAspectRatio="none">
<path d="M87.343 2.344S60.996 3.662 44.027 3.937C27.057 4.177.686 3.655.686 3.655c-.913-.032-.907-1.923-.028-1.999 0 0 26.346-1.32 43.315-1.593 16.97-.24 43.342.282 43.342.282.904.184.913 1.86.028 1.999" />
</svg>
</div>
<h1 class="text-4xl font-extrabold text-slate-50 mb-4">The modern way to find high-quality devs</h1>
<p class="text-slate-400 mb-6">We're the world's largest marketplace of quality developers for early-stage startups. Need a hand with development? Grab one of ours!</p>
<a class="text-sm font-medium inline-flex items-center justify-center px-3 py-1.5 border border-slate-700 rounded-lg tracking-normal transition text-slate-300 hover:text-slate-50 group" href="#0">
Learn More <span class="text-slate-600 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">-></span>
</a>
</div>
</div>
<img class="mx-auto max-md:-translate-x-[5%]" src="./illustration-02.png" width="519" height="490" alt="Illustration 02">
</div>
<div class="absolute left-12 bottom-0 h-14 flex items-center text-xs font-medium text-slate-400">02</div>
</div>
</section>
<!-- Section #3 -->
<section>
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out">
<div class="md:flex justify-between items-center">
<div class="shrink-0 px-12 py-14 max-md:pb-0 md:pr-0">
<div class="md:max-w-md">
<div class="font-nycd text-xl text-teal-500 mb-2 relative inline-flex justify-center items-end">
Appealing
<svg class="absolute fill-teal-500 opacity-40 -z-10" xmlns="http://www.w3.org/2000/svg" width="88" height="4" viewBox="0 0 88 4" aria-hidden="true" preserveAspectRatio="none">
<path d="M87.343 2.344S60.996 3.662 44.027 3.937C27.057 4.177.686 3.655.686 3.655c-.913-.032-.907-1.923-.028-1.999 0 0 26.346-1.32 43.315-1.593 16.97-.24 43.342.282 43.342.282.904.184.913 1.86.028 1.999" />
</svg>
</div>
<h1 class="text-4xl font-extrabold text-slate-50 mb-4">The modern way to find high-quality devs</h1>
<p class="text-slate-400 mb-6">We're the world's largest marketplace of quality developers for early-stage startups. Need a hand with development? Grab one of ours!</p>
<a class="text-sm font-medium inline-flex items-center justify-center px-3 py-1.5 border border-slate-700 rounded-lg tracking-normal transition text-slate-300 hover:text-slate-50 group" href="#0">
Learn More <span class="text-slate-600 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">-></span>
</a>
</div>
</div>
<img class="mx-auto max-md:-translate-x-[5%]" src="./illustration-03.png" width="519" height="490" alt="Illustration 03">
</div>
<div class="absolute left-12 bottom-0 h-14 flex items-center text-xs font-medium text-slate-400">03</div>
</div>
</section>
</div>
</div>
The sctucture consists of a container with three sections, each containing an image and some text. The sections are set against a dark background with rounded borders, giving it a sleek look. The sections are stacked one below the other, with a vertical margin of 56px (space-y-14).
Designing the animation
Before we start using Alpine.js for animation, we must first decide how to arrange the cards when they are in âcollapsedâ mode. Instead of using absolute positioning â like in a previous tutorial on creating a sticky on scroll effect â weâll use the CSS property transform: translateY() to shift the cards upwards and stack them on top of each other. This way, we can maintain the containerâs height and easily determine when each section appears in the viewport using the Intersect Plugin.
So, letâs modify the HTML to stack the cards:
<div class="max-w-5xl mx-auto">
<div class="relative z-0 space-y-14">
<!-- Section #1 -->
<section class="[--i:0]">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[2] -translate-y-[calc(100%*var(--i))]">
<!-- Card content -->
</div>
</section>
<!-- Section #2 -->
<section class="[--i:1]">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[1] -translate-y-[calc(100%*var(--i))]">
<!-- Card content -->
</div>
</section>
<!-- Section #3 -->
<section class="[--i:2]">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-0 -translate-y-[calc(100%*var(--i))]">
<!-- Card content -->
</div>
</section>
</div>
</div>
In summary, hereâs what we did:
- Each section is given classes like
[--i:0]
,[--i:1]
, and[--i:2]
. These classes set a custom CSS variable--i
, marking each sectionâs index. The index value will help us to determine the translate-y value for every section. - The direct descendants of each section are given the class
-translate-y-[calc(100%*var(--i))]
, that makes them shift up based on their index. So, the first sectionâs content wonât move up at all (translate-y: -100% * 0
=0
), the second sectionâs content will move up by 100% (translate-y: -100% * 1
=-100%
), and the third sectionâs content will move up by 200% (translate-y: -100% * 2
=-200%
). - As subsequent sections stack over their predecessors, we inverted the stacking order using classes like
z-[2]
,z-[1]
, andz-0
.
This setup ensures our sections appear âcollapsedâ, preparing them for the cascading reveal upon scrolling.
Implementing the animation with Alpine.js and the Intersect plugin
So, hereâs how the animation works: when you scroll down and a certain section is supposed to come into view, the content of that section will slide down to reveal itself, and it will also bring along all the following sections.
To do that, weâll use Alpine.js and the Intersect Plugin â which is a wrapper for the Intersection Observer â to detect when an element appears in the viewport.
First, we need to make sure we load both required libraries. Weâll do that as quickly as possible, by including a script tag in the head of our document.
After ensuring both required libraries are loaded, our next step is to trigger an action when an element appears in the viewport using the x-intersect attribute. This, combined with other techniques, manages the correct translate-y value for each section.
Note: If, by some wild chance, youâve made it this far and are gearing up to say, âHey, why use two whole libraries for such a teeny effect?â â please consider that the main purpose of this tutorial is educational. In the real world, you might already have Alpine.js installed and have reasons to use Intersect Plugin. But if the thought of adding another library for this makes you twitchy, consider this tutorial a starting point to craft your custom solution! đ
Now, letâs get back to it. To perform an action when an element enters the viewport, we need to add the x-intersect
attribute to the element itself. In our case, we want to set a variable (which weâll call entered
) and assign it the index of the last section that entered the field of view. This will help us manage the correct value of translate-y
to assign to each section:
<div class="max-w-5xl mx-auto" x-data="{ entered: '0' }">
<div class="relative z-0 space-y-14">
<!-- Section #1 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '0'" class="[--i:0]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[2] -translate-y-[calc(100%*var(--i))]">
<!-- Card content -->
</div>
</section>
<!-- Section #2 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '1'" class="[--i:1]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[1] -translate-y-[calc(100%*var(--i))]">
<!-- Card content -->
</div>
</section>
<!-- Section #3 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '2'" class="[--i:2]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-0 -translate-y-[calc(100%*var(--i))]">
<!-- Card content -->
</div>
</section>
</div>
</div>
Hereâs a summary of what we did:
- We added the
x-data
directive to the main container, which allows us to define theentered
variable with a default value of0
. - Each section has been assigned the
x-intersect
directive. This will change the value of theentered
variable as we scroll. - We also used the
.margin
modifier to control the rootMargin of the IntersectionObserver. By using the values-70%.0.-30%.0
, weâre telling the IntersectionObserver to trigger the action when the section intersects an imaginary horizontal line positioned 30% from the bottom of the viewport. - Finally, weâve added the
:class
attribute to define a new custom CSS variable--e
. This variable helps us calculate the correcttranslate-y
to assign to each section.
Now we have everything we need to properly translate the content of each section. Letâs complete the integration:
<div class="max-w-5xl mx-auto" x-data="{ entered: '0' }">
<div class="relative z-0 space-y-14">
<!-- Section #1 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '0'" class="[--i:0]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[2]" :class="entered >= 0 ? 'translate-y-0' : '-translate-y-[calc(100%*(var(--i)-var(--e)))]'">
<!-- Card content -->
</div>
</section>
<!-- Section #2 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '1'" class="[--i:1]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[1]" :class="entered >= 1 ? 'translate-y-0' : '-translate-y-[calc(100%*(var(--i)-var(--e)))]'">
<!-- Card content -->
</div>
</section>
<!-- Section #3 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '2'" class="[--i:2]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-0" :class="entered >= 2 ? 'translate-y-0' : '-translate-y-[calc(100%*(var(--i)-var(--e)))]'">
<!-- Card content -->
</div>
</section>
</div>
</div>
We have removed the class -translate-y-[calc(100%*var(--i))]
because we need to handle the translate-y
value dynamically based on the entered
variableâs value.
To explain this further, letâs go through some examples. Letâs say section 1 is in view. In this case, the entered
value will be 0
. So, the translate-y
value will be 0
for section 1, -100%
for section 2, and -200%
for section 3.
Now, if section 2 comes into view, the entered
value becomes 1
. As a result, the translate-y
value will be 0
for both sections 1 and 2, and -100%
for section 3.
Lastly, when section 3 is in view, the entered
value will be 2
. Therefore, the translate-y
value for all three sections â 1, 2, and 3 â will be 0
. In simpler terms, all the cards will be visible!
One important thing to note is that this animation occurs every time a section enters the viewport. If you want the animation to happen only once, you can use Alpine.jsâs .once
modifier.
Here is the final code:
<div class="max-w-5xl mx-auto">
<div class="relative z-0 space-y-14" x-data="{ entered: '0' }">
<!-- Section #1 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '0'" class="[--i:0]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[2]" :class="entered >= 0 ? 'translate-y-0' : '-translate-y-[calc(100%*(var(--i)-var(--e)))]'">
<div class="md:flex justify-between items-center">
<div class="shrink-0 px-12 py-14 max-md:pb-0 md:pr-0">
<div class="md:max-w-md">
<div class="font-nycd text-xl text-indigo-500 mb-2 relative inline-flex justify-center items-end">
Interesting
<svg class="absolute fill-indigo-500 opacity-40 -z-10" xmlns="http://www.w3.org/2000/svg" width="88" height="4" viewBox="0 0 88 4" aria-hidden="true" preserveAspectRatio="none">
<path d="M87.343 2.344S60.996 3.662 44.027 3.937C27.057 4.177.686 3.655.686 3.655c-.913-.032-.907-1.923-.028-1.999 0 0 26.346-1.32 43.315-1.593 16.97-.24 43.342.282 43.342.282.904.184.913 1.86.028 1.999" />
</svg>
</div>
<h1 class="text-4xl font-extrabold text-slate-50 mb-4">The modern way to find high-quality devs</h1>
<p class="text-slate-400 mb-6">We're the world's largest marketplace of quality developers for early-stage startups. Need a hand with development? Grab one of ours!</p>
<a class="text-sm font-medium inline-flex items-center justify-center px-3 py-1.5 border border-slate-700 rounded-lg tracking-normal transition text-slate-300 hover:text-slate-50 group" href="#0">
Learn More <span class="text-slate-600 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">-></span>
</a>
</div>
</div>
<img class="mx-auto max-md:-translate-x-[5%]" src="./illustration-01.png" width="519" height="490" alt="Illustration 01">
</div>
<div class="absolute left-12 bottom-0 h-14 flex items-center text-xs font-medium text-slate-400">01</div>
</div>
</section>
<!-- Section #2 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '1'" class="[--i:1]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-[1]" :class="entered >= 1 ? 'translate-y-0' : '-translate-y-[calc(100%*(var(--i)-var(--e)))]'">
<div class="md:flex justify-between items-center">
<div class="shrink-0 px-12 py-14 max-md:pb-0 md:pr-0">
<div class="md:max-w-md">
<div class="font-nycd text-xl text-sky-500 mb-2 relative inline-flex justify-center items-end">
Engaging
<svg class="absolute fill-sky-500 opacity-40 -z-10" xmlns="http://www.w3.org/2000/svg" width="88" height="4" viewBox="0 0 88 4" aria-hidden="true" preserveAspectRatio="none">
<path d="M87.343 2.344S60.996 3.662 44.027 3.937C27.057 4.177.686 3.655.686 3.655c-.913-.032-.907-1.923-.028-1.999 0 0 26.346-1.32 43.315-1.593 16.97-.24 43.342.282 43.342.282.904.184.913 1.86.028 1.999" />
</svg>
</div>
<h1 class="text-4xl font-extrabold text-slate-50 mb-4">The modern way to find high-quality devs</h1>
<p class="text-slate-400 mb-6">We're the world's largest marketplace of quality developers for early-stage startups. Need a hand with development? Grab one of ours!</p>
<a class="text-sm font-medium inline-flex items-center justify-center px-3 py-1.5 border border-slate-700 rounded-lg tracking-normal transition text-slate-300 hover:text-slate-50 group" href="#0">
Learn More <span class="text-slate-600 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">-></span>
</a>
</div>
</div>
<img class="mx-auto max-md:-translate-x-[5%]" src="./illustration-02.png" width="519" height="490" alt="Illustration 02">
</div>
<div class="absolute left-12 bottom-0 h-14 flex items-center text-xs font-medium text-slate-400">02</div>
</div>
</section>
<!-- Section #3 -->
<section x-intersect.margin.-70%.0.-30%.0="entered = '2'" class="[--i:2]" :class="'[--e:'+entered+']'">
<div class="relative bg-slate-800 rounded-2xl border border-slate-700 overflow-hidden transition-transform duration-700 ease-in-out z-0" :class="entered >= 2 ? 'translate-y-0' : '-translate-y-[calc(100%*(var(--i)-var(--e)))]'">
<div class="md:flex justify-between items-center">
<div class="shrink-0 px-12 py-14 max-md:pb-0 md:pr-0">
<div class="md:max-w-md">
<div class="font-nycd text-xl text-teal-500 mb-2 relative inline-flex justify-center items-end">
Appealing
<svg class="absolute fill-teal-500 opacity-40 -z-10" xmlns="http://www.w3.org/2000/svg" width="88" height="4" viewBox="0 0 88 4" aria-hidden="true" preserveAspectRatio="none">
<path d="M87.343 2.344S60.996 3.662 44.027 3.937C27.057 4.177.686 3.655.686 3.655c-.913-.032-.907-1.923-.028-1.999 0 0 26.346-1.32 43.315-1.593 16.97-.24 43.342.282 43.342.282.904.184.913 1.86.028 1.999" />
</svg>
</div>
<h1 class="text-4xl font-extrabold text-slate-50 mb-4">The modern way to find high-quality devs</h1>
<p class="text-slate-400 mb-6">We're the world's largest marketplace of quality developers for early-stage startups. Need a hand with development? Grab one of ours!</p>
<a class="text-sm font-medium inline-flex items-center justify-center px-3 py-1.5 border border-slate-700 rounded-lg tracking-normal transition text-slate-300 hover:text-slate-50 group" href="#0">
Learn More <span class="text-slate-600 group-hover:translate-x-0.5 transition-transform duration-150 ease-in-out ml-1">-></span>
</a>
</div>
</div>
<img class="mx-auto max-md:-translate-x-[5%]" src="./illustration-03.png" width="519" height="490" alt="Illustration 03">
</div>
<div class="absolute left-12 bottom-0 h-14 flex items-center text-xs font-medium text-slate-400">03</div>
</div>
</section>
</div>
</div>
Conclusions
You may have noticed that in our latest Tailwind tutorials, we have been pushing the boundaries a bit. Our goal is to provide more original content that can give you new ideas for your landing pages. If youâre enjoying these experiments and would like to see them included in our Tailwind Templates, let us know. We really love getting feedback from you guys in our community and weâre always working hard to make our products better and more suited to what you want!