Framer motion playground

This is a playground that uses framer motion for various UI animations.

Notification

A simple example of notification that uses framer motion for animations.

Show code
"use client";

import { type DragHandlers } from "framer-motion";

import { useState } from "react";
import { motion, useAnimate, AnimatePresence } from "framer-motion";

import { IconX } from "~/icons/x.icon";
import { IconEmail } from "~/icons/email.icon";

const Notification = () => {
  const [scope, animate] = useAnimate();
  const [active, setActive] = useState(false);

  const handleDragEnd: DragHandlers["onDragEnd"] = (_, info) => {
    const offset = info.offset.x;
    const velocity = info.velocity.x;

    if (offset > 200 || velocity > 500) {
      animate(scope.current, { x: "120%" }, { duration: 0.2 });
      setTimeout(() => setActive(false), 200);
    } else {
      animate(scope.current, { x: 0, opacity: 1 }, { duration: 0.5 });
    }
  };

  return (
    <div className="relative flex items-center justify-center w-full min-h-28 overflow-hidden">
      <motion.button
        type="button"
        layoutId="notification"
        whileTap={{ scale: 0.9 }}
        onClick={() => setActive(!active)}
        className="bg-white px-4 py-2 text-black rounded-md text-sm font-medium truncate"
      >
        <motion.span>Send email</motion.span>
      </motion.button>

      <div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 max-w-full">
        <AnimatePresence>
          {active && (
            <motion.div
              drag="x"
              role="alert"
              aria-live="polite"
              ref={scope}
              dragDirectionLock
              layoutId="notification"
              onDragEnd={handleDragEnd}
              dragConstraints={{ left: 0, right: 200 }}
              className="bg-white rounded-xl text-black py-3 pl-4 pr-12 relative w-full max-w-full"
            >
              <motion.div className="flex flex-row gap-3">
                <motion.div
                  exit={{ x: 20, opacity: 0 }}
                  initial={{ x: -20, opacity: 0 }}
                  animate={{ x: 0, opacity: 1, transition: { delay: 0.3 } }}
                >
                  <IconEmail
                    aria-hidden
                    className="text-primary size-7 -translate-y-0.5"
                  />
                </motion.div>

                <motion.div className="flex flex-col gap-y-0.5">
                  <motion.h3
                    exit={{ x: 20, opacity: 0 }}
                    initial={{ x: -20, opacity: 0 }}
                    className="text-sm font-medium truncate"
                    animate={{ x: 0, opacity: 1, transition: { delay: 0.3 } }}
                  >
                    Email sent successfully
                  </motion.h3>

                  <motion.p
                    exit={{ x: 20, opacity: 0 }}
                    initial={{ x: -20, opacity: 0 }}
                    animate={{ x: 0, opacity: 1, transition: { delay: 0.3 } }}
                    className="text-xs font-medium text-black/60 truncate"
                  >
                    Your email has been sent successfully.
                  </motion.p>
                </motion.div>
              </motion.div>

              <motion.button
                type="button"
                aria-label="Close notification"
                onClick={() => setActive(false)}
                whileTap={{ scale: 0.6 }}
                exit={{ x: -10, opacity: 0 }}
                initial={{ x: 10, opacity: 0 }}
                animate={{ x: 0, opacity: 1, transition: { delay: 0.3 } }}
                className="text-xs font-medium size-5 absolute top-2 right-2 flex items-center justify-center"
              >
                <IconX className="grow-0 shrink-0 size-4" aria-hidden />
              </motion.button>
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
};

export { Notification };