5
2

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 1 year has passed since last update.

Framer Motionを使ったアニメーション例

Posted at

Reactでポートフォリオ作成時に「Framer Motion」を使ってアニメーションをつけたので、それをこの記事で紹介します。

この記事で紹介するアニメーション例は下記になります。

  1. 画面表示時にテキストを順番に表示させるアニメーション
  2. スクロール時に要素を拡大・縮小するアニメーション
  3. 現在表示している要素に応じたヘッダーリンクの色を変えるアニメーション

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を使用しているので割愛。)

ezgif.com-video-to-gif.gif

まずはHome.tsxのコンポーネントを作成しApp.tsxに読み込ませた後、下記のインポートをHome.tsxに行います。

Home.tsx
import { useEffect } from "react";
import { motion, useAnimationControls } from "framer-motion";

useEffectはページを表示した初回のみアニメーションさせるため読み込みます。
ここではmotionの他にuseAnimationControlsも読み込みます。

useAnimationControls

useAnimationControlsは1つ、または複数のアニメーションの開始と停止を制御できる関数です。
この関数を使うことによってアニメーションの開始タイミングをずらし、イメージのようなアニメーションができます。

使い方はuseAnimationControls()を呼び出してアニメーションコントロールを作成します。

Home.tsx
export const Home = () => {
    const controls = useAnimationControls();
}

アニメーションさせる要素を作成

次にアニメーションさせる要素を記述していきます。

Home.tsx
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>に変更し、その中に下記の記述を行います。

Home.tsx
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>の中の属性は下記の意味があります。

  • customuseAnimationControls()で使用する数字です。(後述)
  • initial:その要素の初期の状態を設定できます。直接スタイルを指定することも、後述のvariantsで設定したオブジェクトのkey名を指定することもできます。
  • animate:ここに設定した内容に向かってアニメーションを行います。直接スタイルを指定することや、オブジェクトもしくは作成したコントローラーを指定することもできます。
  • variants:ここにオブジェクトを設定するとinitialanimateに直接スタイルを書かずとも、設定したオブジェクト内のkey名を記載するだけで、設定するスタイルの内容を反映することができます。

variantsに設定するオブジェクトの作成

次にvariantsに設定するオブジェクトを作成します。

Home.tsx
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の中にアニメーションの内容を記載していきます。

Home.tsx
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秒間かけて「表示なし→ぼやけたテキスト→くっきりとしたテキスト」というようにアニメーションしています。
現状では下記のようになっています。

ezgif.com-crop.gif

スクロール時に要素を拡大・縮小するアニメーション

ここでは横にスクロールする時に要素を拡大・縮小するアニメーションの付け方について記載します。
イメージとしては下記の動画のものになります。

ezgif.com-video-to-gif (1).gif

まずは横スクロールできるようにElement.tsxコンポーネントとスタイルを追加します。

App.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
Element.tsx
export const Element = () => {
    return(
        <div className="common__container">
            <div className="element">
                <h2>text5</h2>
            </div>
        </div>
    )
}

横スクロールさせるためのスタイルは下記のように当てておきます。

common.scss
.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;
    }
}

ここまでで下記のような挙動になります。
ezgif.com-video-to-gif (3).gif

ここから下記の手順でイメージのアニメーションを付けていきます。

  1. マウスホイールでページが横にスライドするように変更
  2. HomeコンポーネントとElementコンポーネントにuseRefを設定する
  3. Intersection Observer APIを使って画面に表示されているコンポーネントがどれか判定する
  4. Framer Motionでスライド時に要素が縮小するようにアニメーションをつける

1.マウスホイールでページが横にスライドするように変更

この部分はこちらの記事を参考に作成しています。

下記がコードになります。

App.tsx
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!.scrollLeftMath.ceilMath.floorで囲っている部分です。
これはブラウザの横幅によっては、screenRef.current!.scrollLeftでとれる値に若干の増減があり、delta / window.innerWidthを行ったときに値が整数でないとスクロールしないことがあったためです。

2.HomeコンポーネントとElementコンポーネントにuseRefを設定する

3.でIntersection Observer APIを使ってHomeコンポーネントとElementコンポーネントを取得するためにuseRefを設定していきます。

App.tsx
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を各コンポーネントで受け取っていきます。

Home.tsx
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>
    )
})
Element.tsx
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の使い方はこの記事では割愛します。

useIntersectionObserver.ts
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を作成します。

App.tsx
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の条件に一致している場合isIntersectingtrueにします。
後にこのisIntersectingtrueになっていると要素を拡大表示、falseになっていると縮小表示というようにアニメーションさせていきます。

useIntersectionObserverをApp.tsxで呼び出す

次にuseIntersectionObserverをApp.tsxで呼び出し、上記で作成したcallback関数を使って、拡縮したい要素が画面の98%以上表示されていたらisIntersectingtrueに、そうでなければfalseにするように設定していきます。

App.tsx
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コンポーネントを最初に持ってくると

  1. 画面の98%以上を占めるHomeコンポーネントを読み込み + isIntersectingの初期値がtrueのためisIntersectingtrue
  2. 画面の98%を占めないElementコンポーネントを読み込んだためisIntersectingfalseに変更

という流れで最終的にisIntersectingの値がfalseになってしまいます。

useIntersectionObserver(
    [homeRef, elementRef], // ここの順番が大事
    callback,
    { root: null, rootMargin: "0px", threshold: 0.98 }
);

すると、特にスクロールしていないのに最初からHomeコンポーネントが縮小された状態で表示されるようになってしまいます。

ここまでで、 Homeコンポーネントか、Elementコンポーネントの要素が画面の98%以上に表示されている時にisIntersectingtrueになるようになりました。

4.Framer Motionでスライド時に要素が縮小するようにアニメーションをつける

コンポーネントを拡縮するコンポーネントを作成

ここでいよいよ各コンポーネントを拡縮するコンポーネントを作成します。
アニメーション自体は「テキストを順番に表示するアニメーション」でやっていることそう変わりません。

SlideScaleChange.tsx
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を各コンポーネントに渡す

SlideScaleChangeisIntersectingを使用するために各コンポーネントにisIntersectingを渡していきます。

App.tsx
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にさらに受け渡していきます。

Home.tsx
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>
    )
})
Element.tsx
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というファイルを作成してその中で下記のように定義しています。

Props.ts
export type ContainerProps = {
    isIntersecting: boolean;
}

最後にHomeコンポーネントとElementコンポーネントが縮小した時の見た目ようにわかりやすいようにcommon.scssにスタイルを追加します。

common.scss
.common {

    ~省略~

    &__container {
        width: 100%;
        height: 100vh;
        /* 以下を追加 */
        display: flex;
        justify-content: center;
        align-items: center; 
        background-color: #7d7d7d; 
    }
}

これでスクロール時に要素を拡大・縮小するアニメーションの作成が完了しました。
ここまでで下記のように動作します。

ezgif.com-video-to-gif (4).gif

現在表示している要素に応じたヘッダーリンクの色を変えるアニメーション

ここでは下記の動画の上部にあるヘッダーのアニメーションについて紹介します。
現在表示されているコンポーネントに対応したヘッダーリンクのテキストと背景色が変わるようになります。

ezgif.com-video-to-gif (1).gif

ヘッダーを作成する

初めにヘッダーリンクを作成します。
これまで作成してきたHomeコンポーネントとElementコンポーネントに対応したヘッダーを作成します。

まず、Headerコンポーネントをsrc/component配下に作成します。

Header.tsx
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でヘッダーリンクを作っています。
現在はnavListsclassは使用していませんが、アニメーションをつける段階で使用しますので一旦そのままにしておいてください。
また、aタグの中に空のspanタグがありますが、こちらもアニメーション作成時に使用します。

cssはsrc/styles/header.scssを作成して下記のようにしております。

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をつけます。

Home.tsx
export const Home = forwardRef<HTMLDivElement, ContainerProps>(
    ({isIntersecting}, ref) => {

    ~省略~

    return(
        // id="home"を追加
        <div id="home" ref={ref} className="common__container">
            ~省略~
        </div>
    )
})
Element.tsx
export const Element = forwardRef<HTMLDivElement, ContainerProps>(
    ({isIntersecting}, ref) => {
        return(
            // id="element"を追加
            <div id="element" ref={ref} className="common__container">
                ~省略~
            </div>
        )
    }
)

最後にApp.tsxHeaderコンポーネントを読み込みます。

App.tsx
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の高さになっているため、縮小した時に上の方の余白のみがヘッダー分少なく見え、違和感が出ます。

ezgif.com-video-to-gif (5).gif

そのためHomeコンポーネントとElementコンポーネントをヘッダー分下げるため下記のようにスタイルを追加します。

common.scss
.common {

    ~省略~

    &__container {
        width: 100%;
        height: calc(100vh - 4rem); // 変更
        display: flex;
        justify-content: center;
        align-items: center;
        background-color: #7d7d7d;
        margin-top: 4rem; // 追加
    }
}

これでヘッダーの作成自体は完了になります。
次からアニメーションをつけていきます。

ヘッダーにアニメーションをつける

ヘッダーにアニメーションをつけるために下記のように実装していきます。

  1. 現在画面上に表示されているコンポーネント名が何かを取得する
  2. 現在表示中のコンポーネント名と対応したヘッダーリンクを結びつける
  3. アニメーションをつける

画面に表示中のコンポーネント名取得する

画面に表示中のコンポーネント名を取得するには前項で作成したIntersection Observer APIを使用します。
まずは現在の表示中のコンポーネント名を保存するStateを作成します。

App.tsx
const App = () => {
  const [isIntersecting, setIsIntersecting] = useState<boolean>(true);
  const [currentTab, setCurrentTab] = useState<string>("home"); // 追加 

  ~省略~

export default App

次にIntersection Observer APIのcallback関数に下記の処理を追加します。
ヘッダーを作成時にHomeコンポーネントとElementコンポーネントにアンカーリンクさせるためのidを追加しましたが、現在表示中のコンポーネント(=isIntersectingtrueになっているコンポーネント)のidを取得することによって、何のコンポーネントが表示されているのかがわかります。

App.tsx
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

表示中のコンポーネント名と対応したヘッダーリンクを結びつける

上記で取得したコンポーネント名とisIntersectingHeaderコンポーネントに送ります。
isIntersectingはここでは使用しませんがアニメーションをつける項目で使用します。)

App.tsx
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
Header.tsx
import { FC } from "react"; // 追加
import { HeaderProps } from '../types/Props'; // 追加

// 型とPropsを追加
export const Header: FC<HeaderProps> = (currentTab, isIntersecting ) => {

    ~省略~

}

HeaderPropsの型はsrc/types/Props.tsに追加します。

Props.ts
export type HeaderProps = {
    currentTab: string,
    isIntersecting: boolean;
}

ここまでできたら、Framer Motionからmotionをインポートして、aタグ内のspanmotion.spanに変更し、スタイルをつけていきます。

Header.tsx
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タグの高さをもとの位置に戻す

すると下記のような挙動になります。

ezgif.com-video-to-gif (6).gif

アニメーションをつける

上記でつけた変化をアニメーションさせるように変更します。
まずはvariantsに設定するオブジェクトを作成していきます。

Header.tsx
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に設定し、実際にアニメーションさせます。

Header.tsx
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もスクロール中、もしくは現在の表示コンポーネント名と違うヘッダーリンクの場合は表示位置を要素の高さ分下げ、スクロールをしておらず、表示コンポーネント名と同じヘッダーリンクだった場合は要素をもとの位置に戻すようにしています。
これで下記のようにスクロールに合わせてヘッダーにアニメーションがつくようになりました。

ezgif.com-video-to-gif.gif

以上でFramer Motionを使ったアニメーション例は終了になります。

5
2
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
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?