Fix invisible mobile nav drawer, redesign its open/close as a modern fade

Root cause: the <header> had a fixed h-[6.25rem], not min-h. Its flex-col
first child (top row) has shrink-0 and always fills all 6.25rem, leaving
zero room for the drawer panel — the panel's own overflow-hidden (needed
for its animation) resets flexbox's automatic min-height to 0, so it got
crushed to a literal 0px box regardless of its own max-height. The
hamburger button itself always worked (toggled to "X" correctly); the
panel it opened was rendering at zero height beneath it, at every
breakpoint where it exists (below lg/1024px).

Also replaced the max-height-accordion technique with a CSS grid-rows
(0fr/1fr) transition — no more guessing/capping a max-height — plus an
opacity+translateY fade on the inner content for a softer, more modern
open/close instead of a flat height-only reveal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PL4zfTY1sXc8x5QS6FatM
This commit is contained in:
Marco
2026-07-22 18:24:14 +00:00
parent 39782eeab9
commit b3c44e3082
+94 -67
View File
@@ -345,7 +345,18 @@ export function Navbar({ singleActiveProduct }: { singleActiveProduct: boolean }
// position itself.
<>
<header
className={`sticky top-0 z-50 w-full h-[6.25rem] flex flex-col transition-[background-color,backdrop-filter] duration-300 ${
// min-h-, not a fixed h- — the fixed height used to cap this flex
// column at exactly 6.25rem regardless of content. The top row below
// has shrink-0 (always fills all 6.25rem on its own), so the mobile
// drawer panel — the column's only other child — had zero room left.
// Flex items default to `min-height: auto` (their own content size),
// which normally would've still forced the column taller — but the
// panel's own `overflow-hidden` (needed for its open/close animation)
// resets that automatic minimum to 0 per spec, so flexbox was free to
// crush it to a literal 0px box instead of pushing the header taller.
// The hamburger icon itself was never the problem — it toggled to "X"
// correctly; the panel it opens was rendering at zero height beneath it.
className={`sticky top-0 z-50 w-full min-h-[6.25rem] flex flex-col transition-[background-color,backdrop-filter] duration-300 ${
scrolled || mobileOpen
? "bg-bg-base/80 backdrop-blur-md"
: "bg-bg-base"
@@ -502,78 +513,94 @@ export function Navbar({ singleActiveProduct }: { singleActiveProduct: boolean }
</div>
</div>
{/* Mobile drawer panel — toggleable below lg (see hamburger above) */}
{/* Mobile drawer panel — toggleable below lg (see hamburger above).
`grid-rows-[0fr]/[1fr]` (not a guessed max-h-[Nrem]) is what
actually animates the open/close height — a CSS grid row sized in
`fr` units transitions smoothly between 0 and its content's real
height with no fixed cap to guess/outgrow, the modern replacement
for the old max-height-accordion trick. `overflow-hidden` has to
live on the *inner* div, not this one — collapsing a 0fr grid row
already clips its content on its own, and this outer element is
also where the opacity/translate fade below is applied, which
must NOT itself be clipped (a translateY sliding a hidden/clipped
element in doesn't read as a fade-in, just a hard cut). */}
<div
id="mobile-nav-panel"
ref={panelRef}
className={`lg:hidden w-full overflow-hidden transition-[max-height] duration-300 ease-in-out ${
mobileOpen ? "max-h-[30rem]" : "max-h-0"
className={`lg:hidden grid w-full transition-[grid-template-rows] duration-300 ease-in-out ${
mobileOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
}`}
>
<nav className="flex flex-col gap-6 px-8 pt-2 pb-6">
{navLinks.map((link) => {
const isActive = isNavLinkActive(link.href, pathname, activeSection);
const isHomeAnchor = link.href.startsWith("#") && pathname === "/";
const resolvedHref = link.href.startsWith("#") && pathname !== "/" ? `/${link.href}` : link.href;
return link.href.startsWith("#") ? (
<Link
key={link.href}
href={resolvedHref}
onClick={
isHomeAnchor
? (e) => {
e.preventDefault();
closeMobile();
const el = document.getElementById(link.href.slice(1));
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - NAVBAR_HEIGHT;
smoothScrollTo(top);
<div
className={`overflow-hidden transition-[opacity,transform] duration-300 ease-out ${
mobileOpen ? "opacity-100 translate-y-0 delay-100" : "opacity-0 -translate-y-2"
}`}
>
<nav className="flex flex-col gap-6 px-8 pt-2 pb-6">
{navLinks.map((link) => {
const isActive = isNavLinkActive(link.href, pathname, activeSection);
const isHomeAnchor = link.href.startsWith("#") && pathname === "/";
const resolvedHref = link.href.startsWith("#") && pathname !== "/" ? `/${link.href}` : link.href;
return link.href.startsWith("#") ? (
<Link
key={link.href}
href={resolvedHref}
onClick={
isHomeAnchor
? (e) => {
e.preventDefault();
closeMobile();
const el = document.getElementById(link.href.slice(1));
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - NAVBAR_HEIGHT;
smoothScrollTo(top);
}
history.replaceState(null, "", link.href);
}
history.replaceState(null, "", link.href);
}
: closeMobile
}
className="min-h-11 flex flex-col justify-center gap-1 text-h4 font-semibold text-text-primary w-fit"
>
{link.label}
<span
className={`h-[2px] bg-brand transition-opacity duration-200 ${
isActive ? "w-10 opacity-100" : "w-10 opacity-0"
}`}
/>
</Link>
) : (
<Link
key={link.href}
href={link.href}
onClick={closeMobile}
className="min-h-11 flex items-center text-h4 font-semibold text-text-primary"
>
{link.label}
</Link>
);
})}
</nav>
<div className="flex flex-col gap-3 px-8 pb-8">
<button
type="button"
onClick={() => {
closeMobile();
setNewsletterOpen(true);
}}
className="min-h-11 flex items-center justify-center px-6 py-4 rounded-sm border border-[#868686] text-h4 font-bold text-text-primary hover:border-brand hover:text-brand active:scale-[0.97] transition-all"
>
Newsletter
</button>
<Link
href="/challenge"
onClick={closeMobile}
className="min-h-11 flex items-center justify-center px-6 py-4 rounded-sm bg-brand text-h4 font-bold text-text-primary hover:bg-brand-hover active:scale-[0.97] transition-all"
>
7-Tage-Challenge
</Link>
<div onClick={closeMobile}>
<AccountLink variant="mobile" />
: closeMobile
}
className="min-h-11 flex flex-col justify-center gap-1 text-h4 font-semibold text-text-primary w-fit"
>
{link.label}
<span
className={`h-[2px] bg-brand transition-opacity duration-200 ${
isActive ? "w-10 opacity-100" : "w-10 opacity-0"
}`}
/>
</Link>
) : (
<Link
key={link.href}
href={link.href}
onClick={closeMobile}
className="min-h-11 flex items-center text-h4 font-semibold text-text-primary"
>
{link.label}
</Link>
);
})}
</nav>
<div className="flex flex-col gap-3 px-8 pb-8">
<button
type="button"
onClick={() => {
closeMobile();
setNewsletterOpen(true);
}}
className="min-h-11 flex items-center justify-center px-6 py-4 rounded-sm border border-[#868686] text-h4 font-bold text-text-primary hover:border-brand hover:text-brand active:scale-[0.97] transition-all"
>
Newsletter
</button>
<Link
href="/challenge"
onClick={closeMobile}
className="min-h-11 flex items-center justify-center px-6 py-4 rounded-sm bg-brand text-h4 font-bold text-text-primary hover:bg-brand-hover active:scale-[0.97] transition-all"
>
7-Tage-Challenge
</Link>
<div onClick={closeMobile}>
<AccountLink variant="mobile" />
</div>
</div>
</div>
</div>