Framer Motionを使って単調なモーダル表示にアニメーションを適用してみる

# はじめに

こんにちは、Gakken LEAPのフロントエンドエンジニアの Okuma です。

私はフロントエンドエンジニアとして、日頃からサービスのUI/UXを開発することが多いのですが、特に視覚的な効果からどのようにユーザーにインパクトを与えるかについて考えたりすることが好きです。
普段Webサービスを触っていてもそういったところに着目してしまいます。

現在業務ではあまりWebアニメーションに触れることはありませんが、プライベートの時間を使って日々調べたり試したりしています。

そこで今回はReactのアニメーションライブラリであるFramer Motionを使ったアニメーションの実装についてお話ししようと思います。

# Framer Motionとは

Framer Motionとは [Framer社](https://www.framer.com/) が開発・提供するオープンソースのReactアニメーションライブラリになります。

Reactに使用されるJavaScriptの構文拡張であるJSX（JavaScript XML）の記法を崩すことなく、比較的シンプルな記述で書くことができるのと、ホバーやクリックなどのマウス動作と連動するようなピンポイントのアニメーションにもFramer Motionは簡単に適用することができます。

導入や適用が容易な反面、アニメーションを定義するプロパティの組み合わせによって様々なアニメーションを再現することができるために奥が深いライブラリだと思っています。

# 土台となるモーダルを作成する

まずは今回Framer Motionを試す上での土台となるモーダルを実装してみました。

余談ですが最近は生のCSSを書く機会はめっきりなくなり、TailwindCSSで完結することがほとんどになっています。先日たまたまCSSを書くことがあったのですが、CSSのプロパティを忘れていてTailwindCSSのドキュメントから逆引きしてCSSを探すような場面がありました。

つくづくTailwindCSSの便利さに頼ってしまっていますが、今回もTailwindCSSを使って簡単なボタンとモーダルを実装してみました。

```jsx
export const Modal = () => {
  const [showModal, setShowModal] = useState(false);

  return (
    <>
      　{/* ボタン */}
      <button
        className="bg-emerald-500 text-white active:bg-emerald-600 font-bold uppercase text-sm mt-5 px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
        type="button"
        onClick={() => setShowModal(true)}
      >
        Open modal
      </button>
      {showModal ? (
        <>
          <div className="justify-center items-center flex overflow-x-hidden overflow-y-auto fixed inset-0 z-50 outline-none focus:outline-none">
            <div className="relative w-auto my-6 mx-auto max-w-lg">
              {/* モーダルの中身 */}
              <div className="border-0 rounded-lg shadow-lg relative flex flex-col w-full bg-white outline-none focus:outline-none">
                {/* モーダルヘッダー */}
                <div className="flex items-start justify-between p-5 border-b border-solid border-blueGray-200 rounded-t">
                  {/* モーダルタイトル */}
                  <h3 className="text-3xl font-semibold">Modal Title</h3>
                  {/* 閉じるボタン */}
                  <button
                    className="p-1 ml-auto bg-transparent border-0 text-black opacity-5 float-right text-3xl leading-none font-semibold outline-none focus:outline-none"
                    onClick={() => setShowModal(false)}
                  >
                    <span className="bg-transparent text-black opacity-5 h-6 w-6 text-2xl block outline-none focus:outline-none">
                      ×
                    </span>
                  </button>
                </div>
                {/* 本文 */}
                <div className="relative p-6 flex-auto">
                  <p className="my-4 text-blueGray-500 text-lg leading-relaxed">
                    Lorem ipsum dolor sit amet consectetur adipisicing elit.
                    Ipsa quisquam quaerat ex at ut nisi dicta, repudiandae porro
                    earum, vero delectus, saepe nihil cupiditate! Ut commodi est
                    quaerat excepturi at!
                  </p>
                </div>
                {/* モーダルフッター */}
                <div className="flex items-center justify-end p-6 border-t border-solid border-blueGray-200 rounded-b">
                  {/* closeボタン */}
                  <button
                    className="text-red-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
                    type="button"
                    onClick={() => setShowModal(false)}
                  >
                    Close
                  </button>
                  {/* save changesボタン */}
                  <button
                    className="bg-emerald-500 text-white active:bg-emerald-600 font-bold uppercase text-sm px-6 py-3 rounded shadow hover:shadow-lg outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150"
                    type="button"
                    onClick={() => setShowModal(false)}
                  >
                    Save Changes
                  </button>
                </div>
              </div>
            </div>
          </div>
          {/* オーバーレイ */}
          <div className="opacity-25 fixed inset-0 z-40 bg-black"></div>
        </>
      ) : null}
    </>
  );
};
```

https://codesandbox.io/embed/7z7g28?view=preview&module=%2Fsrc%2Fclient.tsx

特にアニメーションを実装していないこの状態だと「Open Modal」のボタンを押下すると、すぐにモーダルが表示されます。

もちろんこれでも立派なモーダルですし、表示や機能としては問題ありません。
ただ、動きがないとどうしても単調な印象になってしまいます。

# アニメーションの適用（基礎編）

それでは、ここから先ほど作成したモーダルにアニメーションを適用してみましょう。

Framer MotionをJSXのタグに適用するのはとても簡単です。以下のように適用したいタグに対して`motion.`を先頭に付与するだけです。

```jsx
import { motion } from 'framer-motion';
...
<motion.div>Framer Motion</motion.div>
```

あとはFramer Motionが用意しているプロパティを直接タグ内に指定してアニメーションの動きを設定するだけです。

モーダルの表示部分に適用してみます。

```jsx
<motion.div
  className="relative w-auto my-6 mx-auto max-w-lg"
  initial={{ opacity: 0, transform: "translateY(30%)" }}
  animate={{ opacity: 1, transform: "translateY(0)" }}
  transition={{ duration: 0.5 }}
>
  ...モーダルの中身...
</motion.div>
```

ここでは「だんだん不透明になる」かつ「下から移動してくる」動きを掛け合わせた設定を追加してみました。

プロパティだと`initial`にあたるのが「初期状態」で、アニメーション開始前の状態なので、透明（`opacity: 0`）・下にずれている（`transform: 'translateY(30%)'`）を指定しています。
`animate`はアニメーション後の状態を指定します。不明で縦の位置が中央なので`{{ opacity: 1, transform: "translateY(0)" }}`の指定です。

こうしてみるとわかるように`animate`が最終的に表示させたい状態として指定し、`initial`で位置や不透明度に変化をつけてあげることでその差分で簡単にアニメーションを適用させることができます。

`transition`ではアニメーションの継続時間や遅延時間を指定することができます。

実際に動作させると以下のような動きになります。

https://codesandbox.io/embed/4yqtf2?view=preview&module=%2Fsrc%2Fclient.tsx

Framer Motionではこのようなプロパティの組み合わせや掛け合いによって様々なアニメーションを適用することができます。

# アニメーションの適用（応用編）

応用編として、他のプロパティを組み合わせたモーダル表示のアニメーションも実装してみましたのでご覧ください。

https://codesandbox.io/embed/v2675t?view=preview&module=%2Fsrc%2Fclient.tsx

先ほどの基礎編で実装したものとは異なり、モーダルが中央からバウンドするように拡大して表示され、さらに閉じる場合も中央に向かって小さくなっていきます。

全体のコードはsandboxの方で見てもらえればと思いますが、ここでは簡単に使用したプロパティなどを紹介します。
今回はモーダル全体をframer motionの`AnimatePresence`というコンポーネントで囲っています。このコンポーネントで囲うことによって、要素のマウント・アンマウント時のアニメーションを定義できるようになります。（アンマウント時のアニメーションを`exit`というプロパティで指定しています）

`AnimatePresence`を使用する際に一つ注意点があり、今回のようなstateの値（`showModal`）の条件分岐によって要素を表示させている場合、その条件分岐ごと`AnimatePresence`で囲う必要があります。

さらにはアニメーションの`initial`、`animate`、`exit`を以下のようにコンポーネントの先頭で定義しました。
アニメーションが複雑になると指定するプロパティの量も比例して増えるため、このように別のオブジェクトで定義しておくことでコードをスッキリさせることもできます。

```jsx
const variants = {
  hidden: {
    opacity: 0.5,
    scale: 0,
  },
  visible: {
    opacity: 1,
    scale: 1,
    // アニメーションの長さや感覚などの動きに関する設定
    transition: {
      type: "spring",
      duration: 0.4,
      stiffness: 250,
      damping: 12,
    },
  },
  exit: {
    opacity: 0,
    scale: 0.2,
    transition: {
      duration: 0.25,
      type: "tween",
      ease: [0.76, 0, 0.24, 1],
    },
  },
};
```

今回は詳しく解説はしませんが、上記のコードでコメントしているように`transition`というプロパティを使用してアニメーションの動きに関する値を調整しています。
`stiffness`や`damping`の値でバウンドするようなアニメーションは表現しています。

# まとめ

今回はFramer Motionによる基礎的なアニメーションの適用方法から少し踏み込んだプロパティを使用したアニメーションについてを解説させていただきました。

もちろんUIのアニメーションはそのサービスにとって必要不可欠な要素では全くありません。しかし、ユーザーに対して少しでも好印象なフィードバックを与えられるのであれば、追加する意味や効果はあると私は思っています。

必要な時に実装できるようにこれからもこのあたりの技術は追っていこうと思います！

# エンジニア募集中

Gakken LEAP では教育をアップデートしていきたいエンジニアを絶賛大募集しています！！
ぜひお気軽にカジュアル面談へお越しください！！

https://gakken-leap.co.jp/recruit/