Resizable Panel

A container that automatically animates its height when its content changes.

Demo

Code

Learn how to build this in Lesson 7 of Framer Motion Recipes.

// resizable-panel.tsx

"use client";

import { AnimatePresence, motion } from "framer-motion";
import { ComponentProps, ReactNode, createContext, useContext } from "react";
import useMeasure from "react-use-measure";

let PanelContext = createContext({ value: "" });

export function Root({
  children,
  value,
  ...rest
}: {
  children: ReactNode;
  value: string;
} & ComponentProps<"div">) {
  let [ref, bounds] = useMeasure();

  return (
    <motion.div
      animate={{ height: bounds.height > 0 ? bounds.height : undefined }}
      transition={{ type: "spring", bounce: 0, duration: 0.8 }}
      style={{ overflow: "hidden", position: "relative" }}
    >
      <div ref={ref}>
        <PanelContext.Provider value={{ value }}>
          <div {...rest}>{children}</div>
        </PanelContext.Provider>
      </div>
    </motion.div>
  );
}

export function Content({
  value,
  children,
  ...rest
}: {
  value: string;
  children: ReactNode;
} & ComponentProps<"div">) {
  let panelContext = useContext(PanelContext);
  let isActive = panelContext.value === value;

  return (
    <AnimatePresence mode="popLayout" initial={false}>
      {isActive && (
        <motion.div
          initial={{ opacity: 0 }}
          animate={{
            opacity: 1,
            transition: {
              type: "ease",
              ease: "easeInOut",
              duration: 0.3,
              delay: 0.2,
            },
          }}
          exit={{
            opacity: 0,
            transition: {
              type: "ease",
              ease: "easeInOut",
              duration: 0.2,
            },
          }}
        >
          <div {...rest}>{children}</div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

Usage

"use client";

import { useState } from "react";
import * as ResizablePanel from "./resizable-panel";

export default function Page() {
  let [state, setState] = useState<"form" | "success">("form");

  return (
    <ResizablePanel.Root value={state}>
      <ResizablePanel.Content value="form">
        <p>Reset password</p>
        <label>
          Email address
          <input type="text" />
        </label>
        <button
          onClick={async () => {
            await sendEmail();
            setState("success");
          }}
        >
          Reset your password
        </button>
      </ResizablePanel.Content>
      <ResizablePanel.Content value="success">
        <p>Email sent!</p>
      </ResizablePanel.Content>
    </ResizablePanel.Root>
  );
}

Related course

Framer Motion Recipes

Add beautiful animations to your React apps using Framer Motion.

8 Lessons
3h 8m
Framer Motion Recipes

Get our latest in your inbox.

Join our newsletter to hear about Sam and Ryan's newest blog posts, code recipes, and videos.