かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React Framer Motion を使ってふわっと出てくるモーダルやメニューを作る

React で Modal や Drawer で作る時に state の変更で表示 / 非表示を切り替えるとパッ出たり消えたりするので UX 的に美しくないからアニメーションをさせたい。
chakra UI のような出来上がっている UI ライブラリ無しで実装してみたのでメモ (chakra UI使うと楽なのだが)

Framer Motion を使う

animation を時前で実装すると CSS アニメーションを待つタイマーとかいろいろと状態を作らないと大変そうだし、メンテコストも高そうだったのでライブラリを使うことにした。車輪の再発明をしてる暇はなかった。

Rect で使える animation ライブラリはちょっと検索しただけでもたくさん出てきたが、 Reactアニメーションライブラリって結局何がいいの? この記事を参考に実装コストが低そうだったので Framer Motion を使うことにした。

onClick で 表示 / 非表示 される Modal コンポーネントを作る

import { FC, ReactNode, useCallback } from 'react';
import { createPortal } from 'react-dom';

const App:FC = () => {
  const { isOpen,  onOpen, onClose } = useDisclosure();
  
  return (
    <>
      <button type={onOpen}>Open Modal</button>
      <Modal isOpen={isOpen} onClose={onClose} />
    </>
  );
};

type ModalProps = {
  isOpen: boolean;
  onClose: () => void;
}

const Modal:FC<ModalProps> = ({ isOpen, onClose }) => {
  return (
     <Portal>
       {isOpen && <div className="modal"></div>}
     </Poral>
  );
}

type PoralProps = { children: ReactNode; };

const Poral: FC<PoralProps> = ({ children }) => {
  return createPortal(children, document.body);
};

const useDisclosure = () => {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  const onClose = useCallback(() => {
    setIsOpen(false);
  }, [setIsOpen]);

  const onOpen = useCallback(() => {
    setIsOpen(true);
  }, [setIsOpen]);

  return { isOpen, onOpen, onClose };
};

Framer Motion で mount / unmount 時のアニメーションを付ける

Framer Motion を使ってコンポーネントの mount / unmount 時にアニメーションを付けるには <AnimatePresence> で囲った中に mount / unmount されるコンポーネント<motion.div> として作成すれば良い

AnimatePresence
Animate components when they're removed from the React tree.
AnimatePresence allows components to animate out when they're removed from the React tree.
cf. AnimatePresence | Framer for Developers

import { motion, AnimatePresence } from "framer-motion"

export const Modal:FC<ModalProps> = ({ isOpen, onClose, children }) => {
  return (
     <Portal>
       <AnimatePresence>
       {isOpen && (
         <motion.div
           className="modal"
           initial={{ opacity: 0, transform: "translateY(50%)", }}
           animate={{ opacity: 1, transform: "translateY(0)",  }}
           exit={{ opacity: 0, transform: "translateY(30%)", }}
         >
           {children}
         </motion.div>
       )}
       </AnimatePresence>
     </Poral>
  );
}

mount 時は initial -> animateCSS アニメーションする・unmound 時は animate -> exitCSS アニメーションする
<motion.div><AnimatePresence> の直下でなくてもOK

いい感じにアニメーションさせようとすると CSSCSS アニメーション力が必要になるが、Framer Motion を使えば React で mount / unmount 時にシンプルにアニメーションを付けることができた

Sample

最近は動画の学習コンテンツ多くて助かりすぎる


[参考]