はじめに
フロントエンド学習歴半年未満の初心者です。現在ポートフォリオを作成していますが
ReactにおいてFramer Motionというライブラリを知ったので使ってみます。
宣言的な構文が特徴で、少ないコード量でリッチなアニメーションが簡単に実装できます。
今回の様に部分的にさりげなく拘ったアニメーションを追加したい場合などに向いているのかなと思いました。
この記事の対象読者
ReactとChakra UIを使っていて、アニメーションを実装したい方。
何を作るのか
おしゃれないいねボタンアニメーションを実装します。(できるとは言っていない)
イメージとしては以下のような、気持ちの良い感じのアニメーションが理想です。
準備
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>
)
}
このように、とても簡単にアニメーションが実装できそうです🤔
実装
上述した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 }
これでとりあえず動作するものになりました。
しかしやはり味気なさを感じてしまいますので改修していきます。
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 }
少し良くなりました。解説していきます。
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
プロパティを
指定しました。 さらにuseAnimation
Hooksを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'],
})
(あまり変わっていない)
本当はオブジェクトを周りに散らしたりしたかったのですが、
上手くいかなかったので一旦諦めます... 気が向いたらまたチャレンジするかもしれません。
さいごに
https://www.framer.com/api/motion/
アニメーションに関しては本当にいろいろなことが出来る様です。
リッチな静的サイトを制作する機会があったら、また触ってみたいと思います。