I’ve built a working animated panel menu in React using Framer Motion. Only one section expands at a time, making the animation smooth and visually clean.
However, I’d like help improving or simplifying the implementation, especially around layout, maxHeight logic, and content rendering. I feel there might be a much better way to achieve this visual effect.
I am also trying to remove the "fold" effect (best seen when expanding Section 3), and would prefer it if other sections slid off the panel rather than shrinking down.
Code:
testmenu.jsx:
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import './testmenu.css';
const App = () => {
const [expandedSection, setExpandedSection] = useState(null);
const sections = [
{
key: 'section1',
title: 'Section 1',
content: <div>This is Section 1 content.</div>
},
{
key: 'section2',
title: 'Section 2',
content: (
<ul>
<li>Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
)
},
{
key: 'section3',
title: 'Section 3',
content: (
<div>
<p>This is a third section with some text and a button:</p>
<button style={{ marginTop: '0.5rem' }}>Click Me</button>
</div>
)
}
];
const expandedIndex = sections.findIndex(s => s.key === expandedSection);
return (
<div className="panel-wrapper">
<motion.div layout className="menu-panel">
<div className="panel-title">Panel Title</div>
<hr className="divider" />
<motion.div layout className="section-stack">
{sections.map(({ key, title, content }, index) => {
const isExpanded = expandedSection === key;
const isAnyExpanded = expandedSection !== null;
const isAbove = isAnyExpanded && index < expandedIndex;
const isBelow = isAnyExpanded && index > expandedIndex;
let maxHeight = '60px';
if (isAnyExpanded) {
if (isExpanded) maxHeight = '600px';
else if (isAbove || isBelow) maxHeight = '0px';
}
return (
<motion.div
key={key}
layout
layoutId={key}
className="section"
animate={{ maxHeight }}
style={{
display: 'flex',
flexDirection: 'column',
flexGrow: isExpanded ? 999 : 0,
minHeight: 0,
overflow: 'hidden',
pointerEvents: !isAnyExpanded || isExpanded ? 'auto' : 'none',
transformOrigin: isAbove ? 'top' : 'bottom',
position: 'relative'
}}
transition={{ duration: 0.5, ease: [0.33, 1, 0.68, 1] }}
>
{/* WRAPPED HEADER to prevent motion dip */}
<motion.div layout="position">
<div
className="section-header"
onClick={() =>
setExpandedSection(isExpanded ? null : key)
}
style={{
height: '60px',
display: 'flex',
alignItems: 'center'
}}
>
{title} {isExpanded ? '▼' : '▶'}
</div>
</motion.div>
{/* Absolutely positioned content */}
<div
className="section-content"
style={{
position: 'absolute',
top: '60px',
left: 0,
right: 0,
display: isExpanded ? 'block' : 'none'
}}
>
{content}
</div>
</motion.div>
);
})}
</motion.div>
</motion.div>
</div>
);
};
export default App;
testmenu.css:
body, html, #root {
margin: 0;
padding: 0;
font-family: sans-serif;
background: #111;
color: white;
height: 100vh;
width: 100vw;
display: flex;
justify-content: center;
align-items: center;
}
.panel-wrapper {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 320px;
height: 240px;
display: flex;
justify-content: center;
align-items: center;
}
.menu-panel {
background: #1e1e2a;
border-radius: 8px;
padding: 1rem;
width: 30vw;
height: 30vh;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
}
.panel-title {
text-align: center;
font-weight: bold;
cursor: pointer;
padding: 0.5rem 0;
}
.section-header {
font-weight: bold;
cursor: pointer;
}
.section-content {
font-size: 0.95rem;
color: #ddd;
margin: 0;
padding: 0;
}
.section-content > *:first-child,
.section-content p:first-child {
margin-top: 0;
}
.section-content > *:last-child,
.section-content p:last-child {
margin-bottom: 0;
}
.section-stack {
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 0;
overflow: hidden;
}
.section {
position: relative;
}
.divider {
border: none;
border-top: 1px solid #444;
margin: 0.5rem 0 0 0;
}
I would appreciate any help towards this.