snaqme Engineers Blog

おやつ体験メーカー snaq.me エンジニアによる開発プロダクトブログ

Framer Motion でアニメーションを実装する

こんにちは。スナックミーでフロントエンドをやっている前田です。
今回は、 snaq.me のマイページで使用している Framer Motion についてご紹介したいと思います。

Framer Motion とは

Framer Motion は複雑なアニメーションを簡単に実装できる React のライブラリです。
シンプルな例では、下記の記述で左右に 100px ずつ跳ねながら移動する円を描画できます。

<motion.div
  style={{
    width: 30,
    height: 30,
    background: "red",
    borderRadius: 30
  }}
  initial={{ translateX: 0 }}
  animate={{ translateX: 100 }}
  transition={{
    type: "spring",
    bounce: 0.5,
    repeatType: "mirror",
    duration: 2,
    repeat: Infinity
  }}
/>

デフォルトでバネのようなエフェクトが使用され、 bounce のオプションに 0~1 の少数値を指定することにより、その動きを制御できます。

bounce: 1 を使用する事はほとんどありませんが、ユーザーにしっかり気づいて欲しいインタラクションには高め(0.6 ぐらい)、自然に振る舞って欲しいときには未指定(デフォルトが 0.2)を使用する事が多いです。

なぜ Framer Motion を使うのか

Web でアニメーションを表現するには、多くの手法が存在します。

  1. gif や スプライト画像でアニメーション
  2. Lottie を使用して JSON ファイルで管理
  3. CSS アニメーション
  4. JavaScript で DOM やスタイルなどを操作

それぞれ一長一短あり、 1 や 2 はデザイナーとの協業が必要となることなどから、 UI デザインに携わらないフロントエンド実装者が主導する場合は 3, 4 の手法を取ることが多いのではないでしょうか。
しかし、どちらも少し手間がかかり、 実装時にアニメーションを定義することを習慣づけるのは難しいと考えています。

どのような手間が必要となるか、例として簡単なフェードアニメーションを実装する場合を見てみましょう。
styled-components では以下のように書けます。

const Block = styled.div<{ isShow: boolean }>`
  opacity: ${({ isShow }) => isShow ? 1 : 0};
  transition: opacity 0.2s ease;
`;

これを Props を受け取らず、レンダリング後にフェードインするように変更するには keyframes を使用します。

const fadeIn = keyframes`
  to { opacity: 1 }
`;

const Block = styled.div`
  opacity: 0;
  animation: ${fadeIn} 0.2s ease-in-out forwards;
`;

opacity の定義が分断されたことにより、少し複雑さが増してしまいました。
さて、このコンポーネントを何らかの UI 操作などでフェードアウトさせたい場合はどうなるでしょう。

const fadeIn = keyframes`
  to { opacity: 1 }
`;

const fadeOut = keyframes`
  from { opacity: 1 }
  to { opacity: 0 }
`;

const Block = styled.div<{ isShow: boolean }>`
  opacity: 0;
  animation: ${({ isShow }) => (isShow ? fadeIn : fadeOut)} 0.2s ease-in-out forwards;
`;

やり方はいろいろあるかと思いますが、今回は animation-name を Props で切り替える仕様にしてみました。
keyframes の定義が増え、fadeOut は from の記述が必須(fadeIn は不要)となるなどさらに複雑性が増したように感じます。
このような手間が必要となれば、つい isShow && <Block /> と書いてしまいたくなるのも頷けるのではないでしょうか。

今度は同様の仕様を Framer Motion で記述してみましょう。

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: isShow ? 1 : 0 }}
/>

だいぶすっきりしましたね。 フェードだけだと描画エリアを確保してしまうので、要素ごと消す場合には以下のように書きます。

<AnimatePresence>
  {isShow && (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    />
  )}
</AnimatePresence>

このように、インタラクションの定義を自然に取り込める記述性の高さと、type: "spring" による目を引くエフェクトの実装しやすさから、 snaq.me では Framer Motion を採用しています。
ここまで小さなサンプルコードで紹介してきましたが、やや実践的なサンプルとして snaq.me で良く使うエフェクトもご紹介します。

Framer Motion を使う場合に考慮すべき点

Framer Motion の良いところばかりを紹介してきましたが、いくつか注意すべき点も存在します。

バンドルサイズが大きい

アニメーションのための機能が数多く提供されている Framer Motion ですが、バンドルサイズが膨らみがちな点が大きなデメリットであると考えています。
Bundlephobia によると、 minified された状態でおよそ 126KB ものファイルサイズになるようです。

bundlephobia.com

この問題については Framer Motion のドキュメントに記載されているので、目的に合わせて設定することでバンドルサイズを減らす事ができます。

www.framer.com

他のライブラリと記法が競合する

Framer Motion では styled(motion.div) などと書くことにより、初期状態のスタイルを styled-components で書くことが可能です。 しかし、 animateexit で指定するプロパティを initial 以外で指定していると正しく動作しないため、 競合しないように管理する必要があります。

// 上手く動かない
<Block animate={{ translateX: 0 }}  />

const Block = styled(motion.div)`
  transform: translateX(100);
`;

これにより、スタイルで JSX の宣言が肥大化して読みづらくなったり、事前に styled-components で定義されているスタイルにアニメーションを付けようとして上手くいかなかったりなどの問題が発生しています。 弊社では Framer Motion を使うときは style を使用するようにして回避していますが、オブジェクト形式で CSS を書くのに抵抗がある方や、デフォルトのスタイルを styled-components に寄せたい場合には問題となる可能性があります。

JSX からスタイルの定義をを分離する手法としては styled-components の attrs を使用する事でも可能ですが、 attrs の generic に MotionProps を渡す手間が発生します。

const Block = styled(motion.div).attrs<unknown, MotionProps>(() => ({
  initial: { opacity: 0 },
  animate: { opacity: 1 },
}))``;

SVG を埋め込みすぎるとバンドルサイズが大きくなる

強力で便利な Framer Motion ですが、 SVG でアニメーションで多用しすぎるとバンドルサイズを大きくする要因となります。 Framer Motion では SVG の path, g, rect, circle などのタグを motion.path, motion.g, motion.g, motion.g などに変えることで個別にアニメーションを指定可能ですが、 d 属性なども同時にバンドルされてしまうため、配信される JS ファイルのサイズに影響を及ぼします。
ファイルサイズの大きい SVG には

  • Dynamic import を使用する
  • ラフ加工されてアンカーポイントの多い SVGPNG にする
  • フロントエンドでのアニメーションは諦める(GIF や Lottie などを使う)

など、状況に合わせた対策が必要です。

まとめ

snaq.me で導入してから2年ほどが経過しましたが、今では snaq.me のマイページの多くの場所で使用しています。
その中でも特に多用しているのが snaq.me の eGift で、オンラインメッセージカードのインタラクションは Framer Motion を使用して実装しています。
近日中に、りっす(弊社の公式タレント)のかわいいクリスマス eGift カードが公開となるので是非覗いてくださいね。
(画像は実装途中の物です)

余談ですが、りっす氏は顔の作りがシンプルでデータ量が少なく、アニメーションを付けやすいのも推しポイントです。