Posted on: 29/05/2026(updated)
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.
This implementation uses three specific techniques to achieve a smooth, weighted feel:
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>
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>
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>
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>
To see this work in a CodePen click See Codepen. 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>