Reactでポートフォリオ作成時に「Framer Motion」を使ってアニメーションをつけたので、それをこの記事で紹介します。
この記事で紹介するアニメーション例は下記になります。
- 画面表示時にテキストを順番に表示させるアニメーション
- スクロール時に要素を拡大・縮小するアニメーション
- 現在表示している要素に応じたヘッダーリンクの色を変えるアニメーション
Framer Motionとは
Framer MotionはReactで使用できるアニメーションライブラリです。
下記の公式ドキュメントでも例が見れますが、簡単な記述で複雑なアニメーションを制御することができます。
Framer Motionの使い方
ここでは簡単に始め方を紹介します。
詳細が知りたい場合は、公式ドキュメントを確認してください。
※Framer Motion には React 18 以降が必要です。
Framer Motionは下記のコマンドを実行するとインストールできます。
npm install framer-motion
インストールが完了したらFramer Motionを使用したいJSXファイルやTSXファイルに下記のインポート文を追加します。
import { motion } from "framer-motion"
上記のインポートしたmotion
をアニメーションさせたい要素のタグに付けます。
例えば特定のdiv
タグにアニメーションを追加したい場合は、そのdiv
を下記のように書き換えます。
<motion.div />
このようにmotion.XXX
とした要素にプロパティを追加していくことでアニメーションをさせることができます。
テキストを順番に表示させるアニメーション
この項目では順番に文字が表示されるアニメーションの付け方について記載します。
イメージとしては下記の動画のものになります。
(雪が舞うようなアニメーションに関してはparticles.jsを使用しているので割愛。)
まずはHome.tsx
のコンポーネントを作成しApp.tsx
に読み込ませた後、下記のインポートをHome.tsx
に行います。
import { useEffect } from "react";
import { motion, useAnimationControls } from "framer-motion";
useEffect
はページを表示した初回のみアニメーションさせるため読み込みます。
ここではmotion
の他にuseAnimationControls
も読み込みます。
useAnimationControls
useAnimationControls
は1つ、または複数のアニメーションの開始と停止を制御できる関数です。
この関数を使うことによってアニメーションの開始タイミングをずらし、イメージのようなアニメーションができます。
使い方はuseAnimationControls()
を呼び出してアニメーションコントロールを作成します。
export const Home = () => {
const controls = useAnimationControls();
}
アニメーションさせる要素を作成
次にアニメーションさせる要素を記述していきます。
export const Home = () => {
const controls = useAnimationControls();
return(
<div className="home">
<div className="home__container">
<h1>
<span>text1</span>
{" "}
<span>text2</span>
</h1>
<h2>
<span>text3</span>
{" "}
<span>text4</span>
</h2>
</div>
</div>
)
}
(CSSの記載は省略してますが)今は下図のような表示になっています。
<span>
を<motion.span>
に変更し、その中に下記の記述を行います。
return(
<div className="common__container">
<div className="home">
<h1>
<motion.span
custom={0}
initial="init"
animate={controls}
variants={textAnimation}
>
text1
</motion.span>
{" "}
<motion.span
custom={1}
initial="init"
animate={controls}
variants={textAnimation}
>
text2
</motion.span>
</h1>
<h2>
<motion.span
custom={2}
initial="init"
animate={controls}
variants={textAnimation}
>
text3
</motion.span>
{" "}
<motion.span
custom={3}
initial="init"
animate={controls}
variants={textAnimation}
>
text4
</motion.span>
</h2>
</div>
</div>
)
<motion.span>
の中の属性は下記の意味があります。
-
custom
:useAnimationControls()
で使用する数字です。(後述) -
initial
:その要素の初期の状態を設定できます。直接スタイルを指定することも、後述のvariants
で設定したオブジェクトのkey名を指定することもできます。 -
animate
:ここに設定した内容に向かってアニメーションを行います。直接スタイルを指定することや、オブジェクトもしくは作成したコントローラーを指定することもできます。 -
variants
:ここにオブジェクトを設定するとinitial
やanimate
に直接スタイルを書かずとも、設定したオブジェクト内のkey名を記載するだけで、設定するスタイルの内容を反映することができます。
variantsに設定するオブジェクトの作成
次にvariantsに設定するオブジェクトを作成します。
const controls = useAnimationControls();
const textAnimation = {
init: {
color: "transparent",
textShadow: "0 0 100px #333, 0 0 100px #333",
opacity: 0,
},
};
return(
~省略~
)
<motion.span>
のvariantsにはtextAnimation
が既に設定されており、initialにその中のinit
が記載されているため<motion.span>
の初期値として下記のスタイルが適用されます。
color: "transparent",
textShadow: "0 0 100px #333, 0 0 100px #333",
opacity: 0,
useEffect内にアニメーションの内容を記載
最後にuseEffectの中にアニメーションの内容を記載していきます。
const controls = useAnimationControls();
const textAnimation = {
init: {
color: "transparent",
textShadow: "0 0 100px #333, 0 0 100px #333",
opacity: 0,
},
};
useEffect(() => {
controls.start((i) => ({
textShadow: [
"0 0 90px #333, 0 0 90px #333",
"0 0 3px #333, 0 0 3px #333",
"0 0 0 #333",
],
opacity: [0, 1, 1],
transition: {
ease: "linear",
duration: 3,
delay: i * 0.1,
},
}));
}, []);
return(
~省略~
)
controls.start
を実行することでアニメーションがスタートします。
引数の(i)
には<motion.span>
内に記述したcustom
の数字が小さい順に入ってき、custom
が小さい要素順にアニメーションが開始されます。
感覚としてはfor文に近しく、transitionのdelayにおいてi * 0.1
としているので0.1sずつ遅れてアニメーションされます。
また、animate
のプロパティには配列で値を渡すことによってキーフレームのようにすることができます。
ですので、上記の例では3秒間かけて「表示なし→ぼやけたテキスト→くっきりとしたテキスト」というようにアニメーションしています。
現状では下記のようになっています。
スクロール時に要素を拡大・縮小するアニメーション
ここでは横にスクロールする時に要素を拡大・縮小するアニメーションの付け方について記載します。
イメージとしては下記の動画のものになります。
まずは横スクロールできるようにElement.tsx
コンポーネントとスタイルを追加します。
import './style.scss'
import { Home } from "./components/Home";
import { Element } from './components/Element'; // 追加
const App = () => {
return (
<>
<div className='common__screen'> // 追加
<div className='common__screen__container'> // 追加
<Home />
<Element /> // 追加
</div>
</div>
</>
)
}
export default App
export const Element = () => {
return(
<div className="common__container">
<div className="element">
<h2>text5</h2>
</div>
</div>
)
}
横スクロールさせるためのスタイルは下記のように当てておきます。
.common {
&__screen {
position: relative;
width: 100vw;
height: 100vh;
overflow: auto;
scroll-behavior: smooth;
&__container {
width: 200vw;
height: 100%;
display: flex;
}
}
&__container {
width: 100%;
height: 100vh;
}
}
ここから下記の手順でイメージのアニメーションを付けていきます。
- マウスホイールでページが横にスライドするように変更
-
Home
コンポーネントとElement
コンポーネントにuseRefを設定する -
Intersection Observer API
を使って画面に表示されているコンポーネントがどれか判定する - Framer Motionでスライド時に要素が縮小するようにアニメーションをつける
1.マウスホイールでページが横にスライドするように変更
この部分はこちらの記事を参考に作成しています。
下記がコードになります。
import './style.scss'
import { Home } from "./components/Home";
import { Element } from './components/Element';
import { useEffect, useRef } from 'react'; // 追加
const App = () => {
const screenRef = useRef<HTMLDivElement>(null); // 追加
// useEffectを追加
useEffect(() => {
screenRef.current!.onwheel = (event) => {
event.preventDefault();
let delta = (event.deltaY / Math.abs(event.deltaY)) * window.innerWidth;
if (delta > 0) {
delta += Math.ceil(screenRef.current!.scrollLeft);
delta = Math.floor(delta / window.innerWidth) * window.innerWidth;
} else {
delta += Math.floor(screenRef.current!.scrollLeft);
delta = Math.ceil(delta / window.innerWidth) * window.innerWidth;
}
screenRef.current!.scrollLeft = delta;
}
}, []);
return (
<>
<div ref={screenRef} className='common__screen'> // refを追加
<div className='common__screen__container'>
<Home />
<Element />
</div>
</div>
</>
)
}
export default App
記事と変えている部分としては、if文の最初のscreenRef.current!.scrollLeft
をMath.ceil
やMath.floor
で囲っている部分です。
これはブラウザの横幅によっては、screenRef.current!.scrollLeft
でとれる値に若干の増減があり、delta / window.innerWidth
を行ったときに値が整数でないとスクロールしないことがあったためです。
2.HomeコンポーネントとElementコンポーネントにuseRefを設定する
3.でIntersection Observer API
を使ってHome
コンポーネントとElement
コンポーネントを取得するためにuseRefを設定していきます。
const App = () => {
const screenRef = useRef<HTMLDivElement>(null);
const homeRef = useRef<HTMLDivElement>(null); // 追加
const elementRef = useRef<HTMLDivElement>(null); // 追加
~省略~
return (
<>
<div ref={screenRef} className='common__screen'>
<div className='common__screen__container'>
<Home ref={homeRef}/> // refのpropsを追加
<Element ref={elementRef}/> // refのpropsを追加
</div>
</div>
</>
)
}
export default App
次にpropsとして設定したref
を各コンポーネントで受け取っていきます。
import { forwardRef, useEffect } from "react"; // 追加
import { motion, useAnimationControls } from "framer-motion";
export const Home = forwardRef<HTMLDivElement>(
({}, ref) => { // forwardRefと{}、refを追加
~省略~
return(
<div ref={ref} className="common__container"> // refを追加
~省略~
</div>
)
})
import { forwardRef } from "react" // 追加
export const Element = forwardRef<HTMLDivElement>(
({}, ref) => { // forwardRefと{}、refを追加
return(
<div ref={ref} className="common__container"> // refを追加
~省略~
</div>
)
}
)
3.Intersection Observer APIを使って画面に表示されているコンポーネントがどれか判定する
カスタムhookを作成
hooks/useIntersectionObserver.ts
というファイルを作ってその中に下記のコードを記載します。
これは配列として受け取った拡縮させたい要素を監視するhookになります。
※Intersection Observer API
の使い方はこの記事では割愛します。
import { RefObject, useEffect } from "react";
export const useIntersectionObserver = (
refs: RefObject<HTMLElement>[],
callback: (entries: IntersectionObserverEntry[]) => void,
options?: IntersectionObserverInit
): void => {
useEffect(() => {
const observer = new IntersectionObserver(callback, options)
refs.forEach((ref) => {
if (ref.current) {
observer.observe(ref.current)
}
})
return () => {
refs.forEach((ref) => {
if (ref.current) {
observer.unobserve(ref.current)
}
})
}
}, [])
}
IntersectionObserver()で実行するcallback関数を作成
次にApp.tsxにcallback関数とそこで使うstateを作成します。
import './style.scss'
import { Home } from "./components/Home";
import { Element } from './components/Element';
import { useEffect, useRef, useState } from 'react'; // useStateを追加
const App = () => {
const [isIntersecting, setIsIntersecting] = useState<boolean>(true); // 追加
// callback関数を作成
const callback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
setIsIntersecting(entry.isIntersecting);
});
};
~省略~
return (
~省略~
)
}
export default App
ここでは監視している要素が次の項目で作成するuseIntersectionObserver
の条件に一致している場合isIntersecting
をtrue
にします。
後にこのisIntersecting
がtrue
になっていると要素を拡大表示、false
になっていると縮小表示というようにアニメーションさせていきます。
useIntersectionObserverをApp.tsxで呼び出す
次にuseIntersectionObserver
をApp.tsxで呼び出し、上記で作成したcallback関数を使って、拡縮したい要素が画面の98%以上表示されていたらisIntersecting
をtrue
に、そうでなければfalse
にするように設定していきます。
import './style.scss'
import { Home } from "./components/Home";
import { Element } from './components/Element';
import { useEffect, useRef, useState } from 'react';
import { useIntersectionObserver } from "./hooks/useIntersectionObserver"; // 追加
const App = () => {
~省略~
// useIntersectionObserverを実行
useIntersectionObserver(
[elementRef, homeRef],
callback,
{ root: null, rootMargin: "0px", threshold: 0.98 }
);
return (
~省略~
)
}
export default App
ここで注意すべきことはuseIntersectionObserver
の引数にref
を設定するとき、ページ表示時に一番最初に表示されるコンポーネント(ここではHome
コンポーネント)のref
は配列の一番最後に設定することです。
ページ表示時、配列の引数に設定した全てのコンポーネントが一度読み込まれ、その読み込み順も配列の先頭から最後にかけてに行なっていくため、仮に下記コードブロックのように一番最初に表示されるHome
コンポーネントを最初に持ってくると
- 画面の98%以上を占める
Home
コンポーネントを読み込み +isIntersecting
の初期値がtrueのためisIntersecting
はtrue
- 画面の98%を占めない
Elementコンポーネント
を読み込んだためisIntersecting
をfalse
に変更
という流れで最終的にisIntersecting
の値がfalse
になってしまいます。
useIntersectionObserver(
[homeRef, elementRef], // ここの順番が大事
callback,
{ root: null, rootMargin: "0px", threshold: 0.98 }
);
すると、特にスクロールしていないのに最初からHome
コンポーネントが縮小された状態で表示されるようになってしまいます。
ここまでで、 Home
コンポーネントか、Element
コンポーネントの要素が画面の98%以上に表示されている時にisIntersecting
がtrue
になるようになりました。
4.Framer Motionでスライド時に要素が縮小するようにアニメーションをつける
コンポーネントを拡縮するコンポーネントを作成
ここでいよいよ各コンポーネントを拡縮するコンポーネントを作成します。
アニメーション自体は「テキストを順番に表示するアニメーション」でやっていることそう変わりません。
import { FC, ReactNode, useEffect } from 'react';
import { motion, useAnimation } from "framer-motion";
type Props = {
children: ReactNode;
isIntersecting: boolean;
}
export const SlideScaleChange: FC<Props> = ({ children, isIntersecting }) => {
// アニメーションの開始と停止を制御できるuseAnimation関数を作成
const control = useAnimation();
// variants用のオブジェクトを作成
const scaleChange = {
reduction: {
height: '80%',
width: '80%',
fontSize: "1.4vh",
},
enlargement: {
height: '100%',
width: '100%',
fontSize: "2vh",
}
}
// isIntersectingがfalseのとき、要素のwidthとheightを80%にして
// font-sizeを1.4vhに変更する
useEffect(() => {
if(!isIntersecting) {
control.start("reduction");
} else {
control.start("enlargement");
}
}, [isIntersecting])
return(
<motion.div initial='enlargement' animate={control} variants={ scaleChange }>{ children }</motion.div>
);
}
isIntersectingを各コンポーネントに渡す
SlideScaleChange
でisIntersecting
を使用するために各コンポーネントにisIntersecting
を渡していきます。
const App = () => {
~省略 ~
return (
<>
<div ref={screenRef} className='common__screen'>
<div className='common__screen__container'>
// isIntersectingを各コンポーネントに渡す
<Home ref={homeRef} isIntersecting={isIntersecting}/>
<Element ref={elementRef} isIntersecting={isIntersecting}/>
</div>
</div>
</>
)
}
export default App
次に各コンポーネントでisIntersecting
を受け取り、SlideScaleChange
にさらに受け渡していきます。
import { forwardRef, useEffect } from "react";
import { motion, useAnimationControls } from "framer-motion";
import { SlideScaleChange } from "./SlideScaleChange"; // 追加
import { ContainerProps } from "../types/Props"; // 追加
export const Home = forwardRef<HTMLDivElement, ContainerProps>(
({isIntersecting}, ref) => { // ContainerProps型とisIntersectingを追加
~省略~
return(
<div ref={ref} className="common__container">
// SlideScaleChangeコンポーネントにisIntersectingを渡し、拡縮させたい要素をchildrenに置く
<SlideScaleChange isIntersecting={isIntersecting}>
<div className="home">
~省略~
</div>
</SlideScaleChange>
</div>
)
})
import { forwardRef } from "react"
import { SlideScaleChange } from "./SlideScaleChange"; // 追加
import { ContainerProps } from "../types/Props"; // 追加
export const Element = forwardRef<HTMLDivElement, ContainerProps>(
({isIntersecting}, ref) => { // ContainerProps型とisIntersectingを追加
return(
<div ref={ref} className="common__container">
// SlideScaleChangeコンポーネントにisIntersectingを渡し、拡縮させたい要素をchildrenに置く
<SlideScaleChange isIntersecting={isIntersecting}>
<div className="element">
<h2>text5</h2>
</div>
</SlideScaleChange>
</div>
)
}
)
上記で追加したContainerProps
型はsrc/types/Props.ts
というファイルを作成してその中で下記のように定義しています。
export type ContainerProps = {
isIntersecting: boolean;
}
最後にHome
コンポーネントとElement
コンポーネントが縮小した時の見た目ようにわかりやすいようにcommon.scss
にスタイルを追加します。
.common {
~省略~
&__container {
width: 100%;
height: 100vh;
/* 以下を追加 */
display: flex;
justify-content: center;
align-items: center;
background-color: #7d7d7d;
}
}
これでスクロール時に要素を拡大・縮小するアニメーションの作成が完了しました。
ここまでで下記のように動作します。
現在表示している要素に応じたヘッダーリンクの色を変えるアニメーション
ここでは下記の動画の上部にあるヘッダーのアニメーションについて紹介します。
現在表示されているコンポーネントに対応したヘッダーリンクのテキストと背景色が変わるようになります。
ヘッダーを作成する
初めにヘッダーリンクを作成します。
これまで作成してきたHome
コンポーネントとElement
コンポーネントに対応したヘッダーを作成します。
まず、Header
コンポーネントをsrc/component
配下に作成します。
export const Header = () => {
const navLists: { text: string; link: string; class: string }[] = [
{ text: 'Home', link: '#home', class: 'home' },
{ text: 'Element', link: '#element', class: 'element' }
]
return(
<header className="header">
<nav className="header__nav">
<ul className="header__nav__wrapper">
{navLists.map((item, index) => {
return (
<li key={index} className="header__nav__list">
<a href={item.link} className="header__nav__list__link">
<span>{item.text}</span>
<span></span>
</a>
</li>
)
})}
</ul>
</nav>
</header>
)
}
navLists
という配列を作ってそれからmap
でヘッダーリンクを作っています。
現在はnavLists
のclass
は使用していませんが、アニメーションをつける段階で使用しますので一旦そのままにしておいてください。
また、a
タグの中に空のspan
タグがありますが、こちらもアニメーション作成時に使用します。
cssはsrc/styles/header.scss
を作成して下記のようにしております。
.header {
display: flex;
position: fixed;
z-index: 0;
justify-content: center;
width: 100%;
height: 4rem;
&__nav {
display: flex;
justify-content: center;
width: 100%;
&__wrapper {
display: flex;
justify-content: center;
width: 100%;
height: 100%;
max-width: 1080px;
}
&__list {
position: relative;
width: 100%;
overflow: hidden;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
&__link {
display: block;
text-align: center;
line-height: 4;
height: 100%;
}
}
}
}
ヘッダーリンクを押したときに対応したコンポーネントへスクロールさせるために各コンポーネントにid
をつけます。
export const Home = forwardRef<HTMLDivElement, ContainerProps>(
({isIntersecting}, ref) => {
~省略~
return(
// id="home"を追加
<div id="home" ref={ref} className="common__container">
~省略~
</div>
)
})
export const Element = forwardRef<HTMLDivElement, ContainerProps>(
({isIntersecting}, ref) => {
return(
// id="element"を追加
<div id="element" ref={ref} className="common__container">
~省略~
</div>
)
}
)
最後にApp.tsx
でHeader
コンポーネントを読み込みます。
const App = () => {
~省略~
return (
<>
<div ref={screenRef} className='common__screen'>
<div className='common__screen__container'>
<Header /> // 追加
<Home ref={homeRef} isIntersecting={isIntersecting}/>
<Element ref={elementRef} isIntersecting={isIntersecting}/>
</div>
</div>
</>
)
}
export default App
また、現在Home
コンポーネントとElement
コンポーネントは拡大表示時に100vhの高さになっているため、縮小した時に上の方の余白のみがヘッダー分少なく見え、違和感が出ます。
そのためHome
コンポーネントとElement
コンポーネントをヘッダー分下げるため下記のようにスタイルを追加します。
.common {
~省略~
&__container {
width: 100%;
height: calc(100vh - 4rem); // 変更
display: flex;
justify-content: center;
align-items: center;
background-color: #7d7d7d;
margin-top: 4rem; // 追加
}
}
これでヘッダーの作成自体は完了になります。
次からアニメーションをつけていきます。
ヘッダーにアニメーションをつける
ヘッダーにアニメーションをつけるために下記のように実装していきます。
- 現在画面上に表示されているコンポーネント名が何かを取得する
- 現在表示中のコンポーネント名と対応したヘッダーリンクを結びつける
- アニメーションをつける
画面に表示中のコンポーネント名取得する
画面に表示中のコンポーネント名を取得するには前項で作成したIntersection Observer API
を使用します。
まずは現在の表示中のコンポーネント名を保存するStateを作成します。
const App = () => {
const [isIntersecting, setIsIntersecting] = useState<boolean>(true);
const [currentTab, setCurrentTab] = useState<string>("home"); // 追加
~省略~
export default App
次にIntersection Observer API
のcallback関数に下記の処理を追加します。
ヘッダーを作成時にHome
コンポーネントとElement
コンポーネントにアンカーリンクさせるためのid
を追加しましたが、現在表示中のコンポーネント(=isIntersecting
がtrue
になっているコンポーネント)のid
を取得することによって、何のコンポーネントが表示されているのかがわかります。
const App = () => {
const [isIntersecting, setIsIntersecting] = useState<boolean>(true);
const [currentTab, setCurrentTab] = useState<string>("home");
const callback = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
setIsIntersecting(entry.isIntersecting);
// ↓を追加。表示されている要素のidをcurrentTabに保存する
if (entry.isIntersecting) {
setCurrentTab(entry.target.id);
}
});
};
}
export default App
表示中のコンポーネント名と対応したヘッダーリンクを結びつける
上記で取得したコンポーネント名とisIntersecting
をHeader
コンポーネントに送ります。
(isIntersecting
はここでは使用しませんがアニメーションをつける項目で使用します。)
const App = () => {
~省略~
return (
<>
<div ref={screenRef} className='common__screen'>
<div className='common__screen__container'>
<Header currentTab={currentTab} isIntersecting={isIntersecting}/> // 追加
<Home ref={homeRef} isIntersecting={isIntersecting}/>
<Element ref={elementRef} isIntersecting={isIntersecting}/>
</div>
</div>
</>
)
}
export default App
import { FC } from "react"; // 追加
import { HeaderProps } from '../types/Props'; // 追加
// 型とPropsを追加
export const Header: FC<HeaderProps> = (currentTab, isIntersecting ) => {
~省略~
}
HeaderProps
の型はsrc/types/Props.ts
に追加します。
export type HeaderProps = {
currentTab: string,
isIntersecting: boolean;
}
ここまでできたら、Framer Motionからmotion
をインポートして、a
タグ内のspan
をmotion.span
に変更し、スタイルをつけていきます。
export const Header: FC<HeaderProps> = ( {currentTab, isIntersecting} ) => {
~省略~
return(
~省略~
return (
<li key={index} className="header__nav__list">
<a href={item.link} className="header__nav__list__link">
// motion.spanに変更し、スタイルを設定
<motion.span
className="header__nav__list__link__text"
style={ currentTab === item.class ? { color: "#fff" } : { color: "#000" }}
>
{item.text}
</motion.span>
<motion.span
style={ currentTab === item.class ? { display: "inline-block", backgroundColor: "#000", width: "100%", height: "100%", y: "-100%",} : { display: "inline-block", backgroundColor: "#000", width: "100%", height: "100%", y: "100%"}}
></motion.span>
</a>
</li>
)
)
}
ここでは現在表示中のコンポーネント名とnavLists
で設定したclassの値が一致しているかどうかで下記の挙動をとるようにしています。
-
コンポーネント名とclassの値が一致していない場合
- 最初の
span
タグのテキストの色を白に設定 - 2番目の
span
タグの背景を黒にし、要素の高さ分位置を下にさげる(header__nav__listに設定したoverflow: hidden;
で表示は見えなくなっています)
- 最初の
-
コンポーネント名とclassの値が一致した場合
- 最初の
span
タグのテキストの色を黒に設定 - 2番目の
span
タグの高さをもとの位置に戻す
- 最初の
すると下記のような挙動になります。
アニメーションをつける
上記でつけた変化をアニメーションさせるように変更します。
まずはvariants
に設定するオブジェクトを作成していきます。
export const Header: FC<HeaderProps> = ( {currentTab, isIntersecting} ) => {
const colorChange = {
inactive: {
color: '#000',
},
active: {
color: '#fff',
}
}
const tabChange = {
inactive: {
y: '100%',
transition: {
duration: .2,
},
},
active: {
y: '-100%',
transition: {
duration: .2,
},
}
}
~省略~
return(
~省略~
)
}
colorChange
は最初のspan
タグのテキストの色を変更し、tabChange
は0.2秒かけて2番目のspan
タグを上下に移動させる記述です。
次にこれらをmotion.span
に設定し、実際にアニメーションさせます。
export const Header: FC<HeaderProps> = ( {currentTab, isIntersecting} ) => {
const colorChange = {
inactive: {
color: '#000',
},
active: {
color: '#fff',
}
}
const tabChange = {
inactive: {
y: '100%',
transition: {
duration: .2,
},
},
active: {
y: '-100%',
transition: {
duration: .2,
},
}
}
~省略~
return(
~省略~
return (
<li key={index} className="header__nav__list">
<a href={item.link} className="header__nav__list__link">
<motion.span
className="header__nav__list__link__text"
style={ currentTab === item.class ? { color: "#fff" } : { color: "#000" }}
animate={!isIntersecting ? "inactive" : currentTab === item.class ? "active" : "inactive"} // 追加
variants={colorChange} // 追加
>{item.text}
</motion.span>
<motion.span
style={ currentTab === item.class ? { display: "inline-block", backgroundColor: "#000", width: "100%", height: "100%", y: "-100%",} : { display: "inline-block", backgroundColor: "#000", width: "100%", height: "100%", y: "100%"}}
animate={!isIntersecting ? "inactive" : currentTab === item.class ? "active" : "inactive"} // 追加
variants={tabChange} // 追加
></motion.span>
</a>
</li>
)
}
最初のmotion.span
ではスクロール中、もしくは現在の表示コンポーネント名と違うヘッダーリンクの場合はテキストの色を黒にし、スクロールをしておらず、表示コンポーネント名と同じヘッダーリンクのテキストの色を白になるようにしています。
次のmotion.span
もスクロール中、もしくは現在の表示コンポーネント名と違うヘッダーリンクの場合は表示位置を要素の高さ分下げ、スクロールをしておらず、表示コンポーネント名と同じヘッダーリンクだった場合は要素をもとの位置に戻すようにしています。
これで下記のようにスクロールに合わせてヘッダーにアニメーションがつくようになりました。
以上でFramer Motionを使ったアニメーション例は終了になります。