Drop-down animation with Tailwind
Smooth drop-down animation without JS libraries
In order to achieve a smooth dropdown animation without a dedicated JavaScript library, we have to address a common CSS limitation:
the height property cannot transition between a fixed value like 0 and auto.
CSS cannot transition between:
height: 0;
height: auto;
This limitation often leads to "snapping" menus or causes developers to reach out for Framer Motion or GSAP just to open a simple dropdown.
1. A Solution with HTML
This implementation uses three specific techniques to achieve a smooth, weighted feel:
I. Custom Timing function
Instead of a generic ease, we use a custom Bézier curve: ease-in-out (cubic-bezier(0.4,0,0.2,1). This mimics real-world
weight-starting fast, and settling with a smooth deceleration
<div
id="dropdown-menu"
class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-in-out overflow-hidden
bg-white border-b shadow-xl">
...
<!-- Dropdown menu content -->
</div>
II. CSS Grid for dynamic height
We animate grid-template-rows instead of the height property.
In CSS Grid, animating from 0fr to 1fr only works, as long as the direct child of
the grid item has min-height:0 (see below) and the container users overflow:hidden.
- Closed: grid-rows[0fr]
- Open: grid-rows[1fr]
The browser calculates the height of the internal content dynamically, and transitions between states correctly.
<div class="min-h-0">
...
<!-- Navigation menu -->
</div>
III. The Staggered effect
To create a cascading reveal effect, the links cannot be static. They must slide up into place while the menu rolls down, and so we use transition-delay classes.
<nav class="flex flex-col p-8 gap-6">
<a
href="#"
class="nav-link text-lg font-semibold border-b pb-2 opacity-0 -translate-y-4
transition-all duration-500 delay-100 text-slate-800">Home
</a>
<a
href="#"
class="nav-link text-lg font-semibold border-b pb-2 opacity-0 -translate-y-4
transition-all duration-500 delay-200ms text-slate-800">Products
</a>
<a
href="#"
class="nav-link bg-indigo-600 text-white text-center py-4 rounded-xl font-bold opacity-0
scale-95 transition-all duration-500 delay-300ms">Sign In
</a>
</nav>
CodePen (HTML/TailwindCSS)
To see this work in a CodePen click here. Or copy/paste the code below into the HTML body of your page, and add the tailwind CDN.
<!-- import tailwindcss -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-slate-50">
<div class="fixed top-0 w-full bg-white shadow-md font-sans z-50"><div class="flex justify-between items-center p-4 border-b bg-white relative z-10">
<span class="font-bold text-xl text-indigo-600">CakeStack®</span><button id="menu-btn" class="p-2 px-4 bg-neutral-100 hover:bg-neutral-200 rounded-md transition-colors font-medium">Menu</button></div>
<div id="dropdown-menu" class="grid grid-rows-[0fr] opacity-0 transition-all duration-500 ease-in-out overflow-hidden bg-white border-b shadow-xl">
<div class="min-h-0"><nav class="flex flex-col p-8 gap-6"><a href="#" class="nav-link text-lg font-semibold border-b pb-2 opacity-0 -translate-y-4 transition-all duration-500 delay-100 text-slate-800"
>Home</a>
<a href="#" class="nav-link bg-indigo-600 text-white text-center py-4 rounded-xl font-bold opacity-0 scale-95 transition-all duration-500 delay-300">Sign In</a>
</nav></div></div>
</div>
<!-- JavaScript -->
<script>
...
// Toggle Curtain Height and Opacity
curtain.classList.toggle("grid-rows-[1fr]", isOpen);
curtain.classList.toggle("grid-rows-[0fr]", !isOpen);
curtain.classList.toggle("opacity-100", isOpen);
curtain.classList.toggle("opacity-0", !isOpen);
});
</script>
</body>
</html>
2. Component Logic with React/NextJS
Ensure the menu wrapper follows this structure:
import Link from 'next/link';
<div className={`grid transition-all duration-500 ease-curtain ${isOpen ? 'grid-rows-[1fr] ' +
' opacity-100'
: 'grid-rows-[0fr] opacity-0'}`}>
<div className="overflow-hidden">
{/* Nav Links with staggered delays */}
<Link className={`transition-all delay-100 ${isOpen ? 'translate-y-0' : 'translate-y-4'}`}>
Home</Link>
<Link className={`transition-all delay-300 ${isOpen ? 'translate-y-0' : 'translate-y-4'}`}>
Sign In</Link>
</div>
</div>
