3
\$\begingroup\$

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.

testmenu in action

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.

\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.