Animated Email Client

An inbox demonstrating enter, update, and exit animations with Framer Motion.

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

"use client";

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ArchiveBoxIcon, EnvelopeIcon } from "@heroicons/react/24/outline";
import { getRandomEmail } from "@/lib/data";

export default function EmailClient({
  initialMessages,
}: {
  initialMessages: { id: string; subject: string; preview: string }[];
}) {
  const [messages, setMessages] = useState(initialMessages);
  const [selectedMessageIds, setSelectedMessageIds] = useState<string[]>([]);

  function toggleMessage(message: {
    id: string;
    subject: string;
    preview: string;
  }) {
    if (selectedMessageIds.includes(message.id)) {
      setSelectedMessageIds((ids) => ids.filter((id) => id !== message.id));
    } else {
      setSelectedMessageIds((ids) => [message.id, ...ids]);
    }
  }

  function addMessage() {
    setMessages((messages) => [...messages, getRandomEmail()]);
  }

  function archiveMessages() {
    setMessages((messages) =>
      messages.filter((message) => !selectedMessageIds.includes(message.id))
    );
    setSelectedMessageIds([]);
  }

  return (
    <div className="flex w-full flex-1 overflow-hidden">
      <div className="flex w-2/5 flex-col bg-gray-800">
        <div className="shadow-sm shadow-black/50 px-5">
          <div className="flex justify-between py-2 text-right">
            <button
              onClick={addMessage}
              className="-mx-2 rounded px-2 py-1 text-gray-500 hover:text-gray-400 active:bg-gray-700 active:text-gray-300"
            >
              <EnvelopeIcon className="size-5" />
            </button>
            <button
              onClick={archiveMessages}
              className="-mx-2 rounded px-2 py-1 text-gray-500 hover:text-gray-400 active:bg-gray-700 active:text-gray-300"
            >
              <ArchiveBoxIcon className="size-5" />
            </button>
          </div>
        </div>

        <ul className="overflow-y-scroll px-3 py-2">
          <AnimatePresence initial={false}>
            {[...messages].reverse().map((message) => (
              <motion.li
                initial={{ height: 0, opacity: 0 }}
                animate={{ height: "auto", opacity: 1 }}
                exit={{ height: 0, opacity: 0, overflow: "hidden" }}
                transition={{ type: "spring", bounce: 0, duration: 0.4 }}
                key={message.id}
                className="relative"
              >
                <div className="py-0.5">
                  <button
                    onClick={() => toggleMessage(message)}
                    className={`${
                      selectedMessageIds.includes(message.id)
                        ? "bg-blue-500"
                        : "hover:bg-gray-700"
                    } block w-full cursor-pointer truncate rounded py-3 px-3 text-left`}
                  >
                    <p
                      className={`${
                        selectedMessageIds.includes(message.id)
                          ? "text-white"
                          : "text-gray-300"
                      } truncate text-sm font-semibold`}
                    >
                      {message.subject}
                    </p>
                    <p
                      className={`${
                        selectedMessageIds.includes(message.id)
                          ? "text-blue-200"
                          : "text-gray-500"
                      } truncate text-xs mt-1`}
                    >
                      {message.preview}
                    </p>
                  </button>
                </div>
              </motion.li>
            ))}
          </AnimatePresence>
        </ul>
      </div>

      <div className="flex-1 overflow-y-scroll border-l-2 border-gray-900/50 px-8 py-8">
        <h1 className="h-8 rounded bg-gray-700 text-2xl font-bold" />
        <div className="mt-8 space-y-6">
          {Array.from(Array(9).keys()).map((i) => (
            <div key={i} className="space-y-2 text-sm">
              <p className="h-4 w-5/6 rounded bg-gray-700" />
              <p className="h-4 rounded bg-gray-700" />
              <p className="h-4 w-4/6 rounded bg-gray-700" />
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

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.