NavigationSimpleApril 7, 2025
Nav Panel Height Expand
A hidden nav panel animates from height:0 to height:auto when the menu is opened. Simultaneously fades in. Collapses back on close. Source: itsjay.us nav dock menu open.
View Full Demo →Preview
Source
demo.jsx
import NavPanelHeightExpand from "./index.jsx";
import styles from "./demo.module.css";
const props = {
siteName: "No-5 Studio",
links: [
{ label: "Work", href: "#" },
{ label: "About", href: "#" },
{ label: "Services", href: "#" },
{ label: "Contact", href: "#" },
],
};
export default function NavPanelHeightExpandDemo() {
return (
<div className={styles.demo}>
<NavPanelHeightExpand {...props} />
<div className={styles.hero}>
<p className={styles.label}>Portfolio — 2024</p>
<h1 className={styles.heading}>Selected Work</h1>
<p className={styles.sub}>Interaction & Visual Design</p>
</div>
</div>
);
}
index.jsx
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import styles from "./styles.module.css";
export default function NavPanelHeightExpand({ siteName = "No-5 Studio", links = [] }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className={styles.wrapper}>
<nav className={styles.navbar}>
<span className={styles.siteName}>{siteName}</span>
<button
className={styles.toggle}
onClick={() => setIsOpen((prev) => !prev)}
aria-expanded={isOpen}
aria-label="Toggle menu"
>
<span className={`${styles.toggleLine} ${isOpen ? styles.open : ""}`} />
<span className={`${styles.toggleLine} ${isOpen ? styles.open : ""}`} />
</button>
</nav>
<motion.div
animate={{ height: isOpen ? "auto" : 0, opacity: isOpen ? 1 : 0 }}
initial={{ height: 0, opacity: 0 }}
transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
className={styles.panel}
>
<ul className={styles.linkList}>
{links.map((link, i) => (
<motion.li
key={link.label}
initial={{ opacity: 0, y: 8 }}
animate={isOpen ? { opacity: 1, y: 0 } : { opacity: 0, y: 8 }}
transition={{
duration: 0.4,
ease: [0.16, 1, 0.3, 1],
delay: isOpen ? 0.15 + i * 0.06 : 0,
}}
>
<a href={link.href} className={styles.link}>
{link.label}
</a>
</motion.li>
))}
</ul>
</motion.div>
</div>
);
}
demo.module.css
.demo {
min-height: 100vh;
background: #f8f8f6;
display: flex;
flex-direction: column;
}
.hero {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 4rem 2rem;
gap: 1rem;
}
.label {
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #9b9b9b;
}
.heading {
font-size: clamp(3rem, 8vw, 7rem);
font-weight: 500;
letter-spacing: -0.03em;
line-height: 0.95;
color: #1a1a1a;
}
.sub {
font-size: 1rem;
font-weight: 400;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #9b9b9b;
}
styles.module.css
.wrapper {
width: 100%;
background-color: #ffffff;
border-bottom: 1px solid #e5e5e5;
}
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 2rem;
max-width: 1100px;
margin: 0 auto;
}
.siteName {
font-size: 1rem;
font-weight: 600;
letter-spacing: -0.01em;
color: #1a1a1a;
}
.toggle {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
width: 28px;
height: 28px;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.toggleLine {
display: block;
width: 100%;
height: 1.5px;
background-color: #1a1a1a;
transition: transform 0.3s ease, opacity 0.3s ease;
transform-origin: center;
}
.toggleLine.open:first-child {
transform: translateY(3.25px) rotate(45deg);
}
.toggleLine.open:last-child {
transform: translateY(-3.25px) rotate(-45deg);
}
.panel {
overflow: hidden;
border-top: 1px solid #e5e5e5;
}
.linkList {
display: flex;
flex-direction: column;
max-width: 1100px;
margin: 0 auto;
padding: 1rem 2rem 1.5rem;
gap: 0.25rem;
}
.link {
display: block;
font-size: 2rem;
font-weight: 500;
letter-spacing: -0.02em;
color: #1a1a1a;
text-decoration: none;
padding: 0.375rem 0;
transition: color 200ms ease;
}
.link:hover {
color: #6b6b6b;
}
Dependencies
framer-motion