profile

Néstor's Blog

React Auto-Resizing Component with Framer Motion

react animation framer-motion

Installation

npm install framer-motion

The Component

import { motion } from 'framer-motion';
import { useRef, useState, useEffect } from 'react';

export function ResizablePanel({ children }: { children: React.ReactNode }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [height, setHeight] = useState<number | 'auto'>('auto');

  useEffect(() => {
    if (!containerRef.current) return;

    const resizeObserver = new ResizeObserver((entries) => {
      // Avoid circular reference by checking if height changed
      const observedHeight = entries[0].contentRect.height;
      if (height !== observedHeight) {
        setHeight(observedHeight);
      }
    });

    resizeObserver.observe(containerRef.current);

    return () => {
      resizeObserver.disconnect();
    };
  }, [height]);

  return (
    <motion.div
      style={{ overflow: 'hidden' }}
      animate={{ height }}
      transition={{
        type: 'spring',
        stiffness: 300,
        damping: 30,
      }}
    >
      <div ref={containerRef}>
        {children}
      </div>
    </motion.div>
  );
}

Usage Examples

Accordion

function Accordion() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setIsOpen(!isOpen)}>
        Toggle
      </button>
      <ResizablePanel>
        {isOpen && (
          <div className="p-4">
            <p>This content animates smoothly</p>
            <p>when shown or hidden.</p>
          </div>
        )}
      </ResizablePanel>
    </div>
  );
}

Filtered List

function FilteredList() {
  const [filter, setFilter] = useState('');
  const items = ['Apple', 'Banana', 'Cherry'];

  const filtered = items.filter(item =>
    item.toLowerCase().includes(filter.toLowerCase())
  );

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filter..."
      />
      <ResizablePanel>
        <ul>
          {filtered.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      </ResizablePanel>
    </div>
  );
}

Expanding Card

function Card() {
  const [expanded, setExpanded] = useState(false);

  return (
    <div className="border rounded">
      <h3>Card Title</h3>
      <ResizablePanel>
        <p>Always visible content</p>
        {expanded && (
          <div>
            <p>Additional details...</p>
            <p>More information...</p>
          </div>
        )}
      </ResizablePanel>
      <button onClick={() => setExpanded(!expanded)}>
        {expanded ? 'Show Less' : 'Show More'}
      </button>
    </div>
  );
}

Customization

Faster Animation

transition={{
  type: 'spring',
  stiffness: 500,  // Higher = faster
  damping: 40,     // Higher = less bounce
}}

Ease Transition

transition={{
  type: 'tween',
  duration: 0.3,
  ease: 'easeInOut',
}}

With AnimatePresence

For mounting/unmounting:

import { AnimatePresence } from 'framer-motion';

function Example() {
  const [show, setShow] = useState(false);

  return (
    <ResizablePanel>
      <AnimatePresence>
        {show && (
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            Content here
          </motion.div>
        )}
      </AnimatePresence>
    </ResizablePanel>
  );
}

Browser Support

ResizeObserver is supported in all modern browsers. For older browsers, use a polyfill.