12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

React / ChakraUI / framer-motion でいいねボタンアニメーション実装

Last updated at Posted at 2021-04-06

はじめに

フロントエンド学習歴半年未満の初心者です。現在ポートフォリオを作成していますが
ReactにおいてFramer Motionというライブラリを知ったので使ってみます。
宣言的な構文が特徴で、少ないコード量でリッチなアニメーションが簡単に実装できます。
今回の様に部分的にさりげなく拘ったアニメーションを追加したい場合などに向いているのかなと思いました。

この記事の対象読者

ReactとChakra UIを使っていて、アニメーションを実装したい方。

何を作るのか

おしゃれないいねボタンアニメーションを実装します。(できるとは言っていない)
イメージとしては以下のような、気持ちの良い感じのアニメーションが理想です。

Twitter
d4lbw-djmtl (1).gif
Zenn
vros9-pz9l1 (1).gif

準備

yarn add @chakra-ui/react framer-motion@3.10.6 react-icons

Chakra UIが使える様になるまでの設定は割愛します

追記

https://github.com/chakra-ui/chakra-ui/issues/3618
framer-motionの最新版v4では本番環境で動作が不安定なようです。
v3.10.6 で動作が確認できているとのことなのでこちらをインストールします。


react-iconsを使い、後ほど2つのハートマークアイコンをimportします。
(枠線のみのハートマークと塗り潰されたハートマーク)

試しに動かしてみる

公式リポジトリにあるQuick Startを参考にとりあえず動かしてみました。

import { motion } from 'framer-motion'
import type { VFC } from 'react'
import { useState } from 'react'

const LikeButtonWithCount: VFC = () => {
  const [isVisible, setIsVisible] = useState(true)

  const toggleVisible = () => {
    setIsVisible(!isVisible)
  }

  return (
    <motion.h1 animate={{ opacity: isVisible ? 1 : 0 }} onClick={toggleVisible}>
      hogehogehoge
    </motion.h1>
  )
}

demooo.gif

このように、とても簡単にアニメーションが実装できそうです🤔

実装

上述した2つのハートマークのアイコン遷移にアニメーションを追加していきたいと思います。
まずはアニメーションのない実装から。
コードが綺麗でない点については何卒ご容赦ください。

import { Box, Icon, Text, Tooltip } from '@chakra-ui/react'
import type { VFC } from 'react'
import { useState } from 'react'
// AiFillHeart => ❤︎
// AiOutlineHeart => ♡
import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'

export type LikeButtonWithCountProps = {
  count: number
  isLiked: boolean
}

const LikeButtonWithCount: VFC<LikeButtonWithCountProps> = (props: LikeButtonWithCountProps) => {
  const [isLike, setIsLike] = useState(false)

  const toggleLike = () => {
    setIsLike(!isLike)
  }

  return (
    <Box display="flex" alignItems="center" color="gray.500">
      <Tooltip label="いいね!" bg="gray.400" fontSize="11px">
        <Text cursor="pointer" onClick={toggleLike}>
          <Icon
            as={isLike ? AiFillHeart : AiOutlineHeart}
            mr="2.5"
            fontSize="22px"
            color={isLike ? 'red.400' : ''}
          />
        </Text>
      </Tooltip>
      <Text>{props.count}</Text>
    </Box>
  )
}

export { LikeButtonWithCount }

4l8au-35ozl (1).gif
これでとりあえず動作するものになりました。
しかしやはり味気なさを感じてしまいますので改修していきます。

import type { HTMLChakraProps } from '@chakra-ui/react'
import { Box, chakra, Icon, Text, Tooltip } from '@chakra-ui/react'
import type { HTMLMotionProps } from 'framer-motion'
import { motion, useAnimation } from 'framer-motion'
import type { VFC } from 'react'
import { useState } from 'react'
import { AiFillHeart, AiOutlineHeart } from 'react-icons/ai'

export type LikeButtonWithCountProps = {
  count: number
  isLiked: boolean
}

const LikeButtonWithCount: VFC<LikeButtonWithCountProps> = (props: LikeButtonWithCountProps) => {
  const [isLike, setIsLike] = useState(false)
  const controls = useAnimation()

  const toggleLike = async () => {
    await setIsLike(!isLike)
    controls.start({ scale: [1, 1.3, 1.6, 1.3, 1] })
  }

  type Merge<P, T> = Omit<P, keyof T> & T
  type MotionBoxProps = Merge<HTMLChakraProps<'div'>, HTMLMotionProps<'div'>>

  const MotionBox: React.FC<MotionBoxProps> = motion(chakra.div)

  return (
    <Box display="flex" alignItems="center" color="gray.500">
      <Tooltip label="いいね!" bg="gray.400" fontSize="11px">
        <MotionBox
          cursor="pointer"
          onClick={toggleLike}
          animate={controls}
          transition={{ duration: 0.2 }}
        >
          <Icon
            as={isLike ? AiFillHeart : AiOutlineHeart}
            mr="2.5"
            fontSize="22px"
            color={isLike ? 'red.400' : ''}
          />
        </MotionBox>
      </Tooltip>
      <Text>{props.count}</Text>
    </Box>
  )
}

export { LikeButtonWithCount }

44 (1).gif

少し良くなりました。解説していきます。

type Merge<P, T> = Omit<P, keyof T> & T
type MotionBoxProps = Merge<HTMLChakraProps<'div'>, HTMLMotionProps<'div'>>

const MotionBox: React.FC<MotionBoxProps> = motion(chakra.div)

こちらはFramerMotionの用意しているdiv要素の型と
Chakra UIの用意しているdiv要素の型をマージしています。
これによってMotionBoxタグではFramer MotionとChakra UIのプロパティが
両方使えるという訳です。

ハートのアイコンをMotionBoxで囲み、onClick,animate,transitionプロパティを
指定しました。 さらにuseAnimationHooksをimportして
ボタンクリックをトリガーとしてアニメーションが発火するようにします。

const toggleLike = async () => {
  await setIsLike(!isLike)
  (!isLike) {
    controls.start({ scale: [1, 1.3, 1.6, 1.3, 1] })
  }
}

クリック後のこちらの関数、未いいねならいいねをつける、いいね済ならいいねを外す
という処理ですがsetIsLikeをawaitさせないと
後続のアニメーションスタート処理がうまく動きません。
ここら辺は勉強中なのですが、この方法でもしかしたら何らかの弊害があるかもしれません...

controls.startにてアニメーションをスタートさせます。
配列でプロパティを渡すことでkeyframesと同じ役割をします。
transition={{ duration: 0.2 }}を指定している為、0.2秒
を5で割った地点でそれぞれの変更がアニメーションされます。

最後にアニメーション部分を修正しました。

 controls.start({
   scale: [0.9, 1.1, 1.2, 1.2, 1],
   color: ['#FFF5F5', '#FED7D7', '#FEB2B2', '#FC8181', '#F56565'],
})

j165v-llims (1).gif
(あまり変わっていない)
本当はオブジェクトを周りに散らしたりしたかったのですが、
上手くいかなかったので一旦諦めます... 気が向いたらまたチャレンジするかもしれません。

さいごに

https://www.framer.com/api/motion/
アニメーションに関しては本当にいろいろなことが出来る様です。
リッチな静的サイトを制作する機会があったら、また触ってみたいと思います。

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?