オンラインでご相談ください!

ご相談内容が完全に固まっていない場合でも
遠慮なくミーティングの予約をどうぞ

TOP デザイン・技術 【React + gsap】でモーダルを作る!

【React + gsap】でモーダルを作る!

tsutsui tsutsui
【React + gsap】でモーダルを作る!

こんにちは、筒井です。

最近Reactのプロジェクトにアサインされたので、
タイムリーな話題として、Reactで基礎的なUIをどのように作るか書きました。

Reactの基礎知識はあるが、それをどのように活かして、実際にUIを作るか…となっている方向けの記事です。

また、BALANCeでは頻繁にgsapを使ってアニメーションを作るので、gsapも使ってみました。

今回は、実装頻度が高いモーダルを例に、ご紹介します。

生のjs

まずは、普通に生のjsで書くとこんな感じです。
モーダルはクラスとして持っておき、必要なところで以下を呼び出してください。

※cssは省略するので、スタイリングは適当にお願いします!

Modal/Controller.js

// モーダルのコントローラー:ロジック部分

import gsap from "gsap/all";
import Renderer from "./Renderer";

export default class Controller {
constructor() {
    // dom
    this.openbtn = document.querySelector(".js-modal_openbtn");
    this.modal = document.querySelector(".js-modal");
    this.closebtn = this.modal.querySelector(".js-modal_closebtn");

   // init
    this.setup();
    this.setEvents();
  }

  setup() {
    this.renderer = new Renderer(this.modal);
  }

 setEvents() { 
    this.openbtn.addEventListener("click", () => { this.open(); }); 
    this.closebtn.addEventListener("click", () => { this.close(); }); 
  }

  open() {
    if (this.tl) this.tl.kill(); 
    this.modal.classList.add("isActive"); // ★

    this.tl = gsap.timeline();
    this.tl.add(this.renderer.open());
  }

  close() {
    if (this.tl) this.tl.kill();
    this.modal.classList.remove("isActive");  // ★

    this.tl = gsap.timeline();
    this.tl.add(this.renderer.close());
  }
}

ロジック部分のコードは上記のとおりです。
.isActiveクラスのつけ外しで、pointer-events: autopointer-events: noneを切り替えています。

コードが長くなってしまったので、表現・演出部分は、以下のRenderer.jsに記載することにします。

Modal/Renderer.js

// モーダルのレンダラー:演出・表現部分

import gsap from "gsap/all";

export default class Renderer {
  constructor(modal) {
    this.bg = modal.querySelector(".js-modal_bg");
    this.contents = modal.querySelector(".js-modal_contents");
  }

  open() {
    const tl = gsap.timeline();

    tl
    // bg
    .to(this.bg, 0.5, {
      opacity: 1,
      ease: "expo.out"
    })
    // contents
    .to(
      this.contents, 0.5, {
      opacity: 1,
      ease: "expo.out"
    },0.2);

    return tl;
  }

  close() {
    const tl = gsap.timeline();

    tl
    // bg
    .to(this.bg, 0.5, {
      opacity: 0,
      ease: "expo.out"
    })
    // contents
    .to(
      this.contents, 0.5, {
       opacity: 0,
       ease: "expo.out"
    }, 0.2);

    return tl;
  }
}

なんだか長くなってしまったのですが、

・openbtnをクリックすると、open()が発火
・closebtnをクリックすると、close()が発火

というシンプルなモーダルの最小コードになります。

React

では、上記をReactで書き直してみます。

親側 : index.jsx

まずはModalをコンポーネントとして持っておき、必要なページの中で呼び出します。
(※ついでに、ボタンもコンポーネント化しちゃいました。)

import style from "../../style/pages/top/top.module.scss";
import { useState } from "react";
import { Button } from "../../components/Button";
import { Modal } from "./Modal/Modal";

export const Top = () => {
   // ①モーダルの状態を用意
  const [isOpen, setModalStatus] = useState(false); 

   // ②クリックでstate(isOpen)をtrueに
  const onClick = () => {
    setModalStatus(true); 
  };

  return (
    <>
      <div className={style.wrap}>
        <div className={style.inner}>
          <div className={style.btnwrap} onClick={onClick}>
            <Button>Open</Button>
          </div>
        </div>
      </div>
      <Modal isOpen={isOpen} setModalStatus={setModalStatus} />
    </>
  );
};

状態の管理は、useStateを用います。
モーダルは最初閉じてるので、初期値はfalseで、useState(false); ですね。

btnwrapをクリックすると、stateが変わるように組んでやります。

ReactではaddEventlistenrは不要で、onClick={onClick}とかけます。
ちなみに、removeEventListenerに関しては、イベントハンドラをつけると、reactがいい感じにremoveしてくれるので何もしなくてokです。

値はpropsを通して、modalコンポーネントに送ってやります。

子側:Modalコンポーネント

生のjsの書き方に合わせて、ロジック部分と演出・表現部分でファイルを切り分けます。

・ロジック部分担当 : Modal/Modal.jsx
・演出部分担当 : Modal/Renderer.js

を用意します。

ですが、Renderer.jsは、生のjsと全く同じコードなので、記載は省略します。
上述のModal/Renderer.jsをご参考ください。

Modal/Modal.jsxは、以下です。

// モーダル:ロジック部分

import style from "../../../style/pages/top/modal.module.scss";
import gsap from "gsap/all";
import classNames from "classnames";
import Renderer from "./Renderer";
import { useEffect, useRef } from "react";
import { Button } from "../../../components/Button";

export const Modal = (props) => {
  // ①propsで親から送られてきた値を取得
  const { isOpen, setModalStatus } = props;

  // ②諸々取得
  const modal = useRef(null); // - modal用意
  const tl = useRef(gsap.timeline()); // - tl用意
  const renderer = useRef(null); // - renderer用意

  // ③rendererを用意(演出・表現部分)
  useEffect(() => {
    renderer.current = new Renderer(modal.current); 
  }, []);

  // ④isOpenに変化があるたびに以下が発火
 //   フラグで発火内容を出し分け
  useEffect(() => { 
    if (isOpen) {
      open();
    } else {
      close();
    }
  }, [isOpen]);

 // ⑤閉じるボタンを押すと、state(isOpen)をfalseにする
  //  そのあとは④が発火
  const onClick = () => { 
   setModalStatus(false);
  };

 // ⑥openとclose用意
  const open = () => {
    tl.current.kill();
    tl.current = gsap.timeline();

    tl.current.add(renderer.current.open());
  };

  const close = () => {
    tl.current.kill();
    tl.current = gsap.timeline();

    tl.current.add(renderer.current.close());
  };

  return (
    <>
      <div
        className={style.modal}
        ref={modal}
        style={isOpen && styles.isActive}
      >
        <div className={classNames([style.bg, "js-modal_bg"])}></div>
        <div className={classNames([style.contents, "js-modal_contents"])}>
          <div className={style.contentsInner}>
            <p className={style.contentsText}>モーダルです</p>
            <div className={style.btnwrap} onClick={onClick}>
              <Button>Close</Button>
            </div>
          </div>
        </div>
      </div>
    </>
  );
};

色々コード内にコメントを残してるのですが、以下で少しだけ補足します。

・tlの用意

const tl = useRef(gsap.timeline());

Reactでは、こんな感じでtlを作ります。
tlには、tl.currentでアクセスできます。

なので、生のjsでtlと書いてたところは全て、tl.currentになります。

・Rendererの用意

const renderer = useRef(null);

rendererは最初nullで用意します。

useEffect(() => { r
  renderer.current = new Renderer(modal.current); 
}, []);

そして、useEffectの中でインスタンス化します。

何度もインスタンス化する必要はないので、第二引数を空の配列にして、初回レンダリングの時だけ発火するようにします。

・stateの変更

useEffect(() => { 
  if (isOpen) { open(); } else { close(); } 
}, [isOpen]);

ボタンがクリックするたびにstate(isOpen)が変わるわけですが、その度に上記が動きます。

あとは、openとcloseの中身を用意してあげるだけです!

以上です。

最後までお読みいただきありがとうございました!

ViteでWordPressの構築環境を作る方法点線が描かれるようなアニメーションの方法【JavaScript】スロットの作り方についてはこちらの記事で解説しています。ぜひこちらもご確認ください。

モーションの制作事例

株式会社アカツキ

項目内容
業種・業界情報・通信業
提供サービスコーポレートサイト
課題・目的UXを重視したコーポレートサイトへ刷新したい
成果・効果モダンなUXを実現し、ファンドの先進的なブランドイメージと快適なユーザー体験を実現

アカツキの投資ファンド「AET FUND」のコーポレートサイトをリニューアルしました。かっちりした印象のサイトに寄せるのではなく、UXのよいモダンなサイトを目指しました。

フィードバック感のあるインタラクションやシームレスな画面遷移、モーショングラフィクスを取り入れたメインビジュアルを実装し、閲覧体験を通じて先進性が伝わる設計にしています。

さらに、UIアニメーションやモーショングラフィクス、3D表現・WebGL開発を活用することで、単なる情報整理にとどまらない表現を取り入れました。投資ファンドとしての信頼感を意識しながら、新しさも感じられる構成としています。

事例の詳細についてはこちらからご確認ください。

株式会社ニューバランス ジャパン

項目内容
業種・業界スポーツシューズメーカー
提供サービスキャンペーンサイト、CMS機能
課題・目的フォトグラファーの世界観を高画質に伝えつつ、大量画像でもスムーズかつ更新しやすいサイトにしたい
成果・効果高速表示する技術と簡易CMSを実装し、ブランドの世界観をスムーズに伝えつつ、柔軟な更新体制を実現

ニューバランス ジャパンの115周年記念キャンペーンサイトを制作しました。115周年の特設サイトとして、ブランドの世界観を伝える表現力と、更新のしやすさの両立が求められていました。

サイトでは、115名分の高画質ポートレートを用いながらも表示速度の低下を抑える工夫を施し、モーションデザインではリズム感やイージングを細かく調整することで、心地よいアニメーション表現を実装しています。

さらに、Googleスプレッドシートに入力した内容を反映できる簡易CMS機能も実装し、大量画像への対応、ブランド表現、公開後の運用性まで考慮したサイトに仕上げています。

事例の詳細についてはこちらからご確認ください。

株式会社スクウェア・エニックス

項目内容
業種・業界ゲーム・エンタテインメント業界
提供サービスUIアニメーション、3D表現
課題・目的「FINAL FANTASY XV ROYAL EDITION」の世界観を表現した特設サイトを制作したい
成果・効果クリスタルの破片が舞う演出を施したバナー表現により、海外Webメディア掲載やアワード受賞を獲得

「FINAL FANTASY XV ROYAL EDITION」の発売に合わせて公開された特設サイトの制作を担当しました。作品の魅力や世界観を伝えるため、印象的なシーンとコピーで構成したバナー画像を約30点デジタルメディアに出稿し、その表現の中にクリスタルの破片が舞う演出を取り入れています。

UIアニメーションや3D表現、独自UIを実装し、静的な商品紹介ではなく、作品への没入感を高めるリッチな体験設計としました。こうした表現設計により、海外のWebメディア掲載やアワード受賞にもつながっています。ビジュアル表現と世界観訴求の両面が印象に残る事例です。

事例の詳細についてはこちらからご確認ください。

株式会社weroll

項目内容
業種・業界広告・デジタルマーケティング
提供サービスモーショングラフィクス、コーポレートサイト
課題・目的ブランドイメージに合うサイトを制作し、更新しやすい仕組みを整えたい
成果・効果有機的に回転する動きを取り入れたサイトを制作し、WordPressで更新性を向上

東京のマーケティングエージェンシー「株式会社weroll」のコーポレートサイトを制作しました。コーポレートサイトは長期運用が前提になるため、印象に残るデザインと、公開後に自社で更新しやすい運用性の両立が求められやすいです。

そのため、CMSとモーショングラフィックスを組み合わせた制作を提案し、今回はWordPressで構築しました。ロゴをはじめ、サイト内のさまざまな箇所に有機的に回転する動きを取り入れることで、社名に含まれる「roll」を印象づける演出に仕上げています。

見た目だけでなく、運用面にも配慮した使いやすいサイトです。

事例の詳細についてはこちらからご確認ください。

エフコープ生活協同組合

項目内容
業種・業界小売業
提供サービスモーショングラフィクス、3D表現
課題・目的サステナブルの取り組みに関心を持ってもらえるサイトを制作したい
成果・効果取り組みを一つの街に見立てて紹介するサイトを制作し、親しみを感じる動きを実装

エフコープ生活協同組合のサステナブルな取り組みを紹介するブランドサイトを制作しました。社会的活動を文章だけで紹介すると単調になりやすいため、訪問者が能動的に体験できるしかけを盛り込み、取り組みへの理解を深めやすい構成にしています。

具体的には、エフコープの多様な活動を一つの街にぎゅっと詰め込んで表現しました。シンボルであるりんごマークをモチーフに、親しみやすく可愛らしい動きを多数実装しています。

特にスマートフォンでは、地図アプリから街を覗くような感覚で閲覧できるよう工夫し、街を巡るように楽しく見られる快適な回遊設計としました。

事例の詳細についてはこちらからご確認ください。

株式会社NTTドコモ

項目内容
業種・業界通信業
提供サービス診断コンテンツ
課題・目的訴求力の高い診断コンテンツを通じて、共感を軸にした新しいスマホ選び体験を提供したい
成果・効果ユーザーの声を活かした共感ベースの診断と3D空間演出で、高いUXと自然な購入導線を実現。
想定以上のROIを達成し、キャンペーン継続決定・追加施策にも発展

NTTドコモの診断キャンペーンサイト「声から選ぶスマホ店」を制作しました。Androidユーザーのリアルな声から共感できる意見を選ぶと、自分に合うスマホが提案される体験を設計し、3D空間上でのリッチな診断コンテンツとして実装しました。

ゲーミフィケーションやSNS連携、独自UIの実装なども取り入れ、診断体験を中心とした構成にしています。LPではバーチャルストアへの導線やAndroid機能の紹介も盛り込み、期待以上のROIを達成しました。

冬と夏に実施されたキャンペーンで評価を得たことから、追加キャンペーンの実施にもつながっています。

事例の詳細についてはこちらからご確認ください。

OCEAN PICTURES

項目内容
業種・業界映像制作
提供サービスCMS機能、見積もりシミュレーター
課題・目的デザインを一新し、実績増加に対応する更新しやすいサイトへリニューアルするとともに、見積もりシミュレーターで商談・営業・サポートの負担を軽減しCV率を向上させたい
成果・効果UX・更新性の向上により社内満足度が高まり、見積もりシミュレーター導入で営業対応時間を約25%削減、CV率を約15%向上

映像制作会社OCEAN PICTURESのコーポレートサイトにおける、デザイン・開発・CMS構築を担当しました。7年ぶりのリニューアルで、デザイン刷新と更新しやすさの両立が求められていました。

そのため、ポップなイラストや遊び心のあるオープニングアニメーションを取り入れ、チームの雰囲気が伝わるサイトに仕上げています。ワークフロー紹介ページでは、打ち合わせから制作・納品までの流れを整理し、わかりやすい導線を設計しました。

さらに、CMS運用を見据えた管理画面のカスタマイズも行い、印象面だけでなく、実績やメンバーの増加にも対応しやすい構成にしています。

事例の詳細についてはこちらからご確認ください。

株式会社イエローハット

項目内容
業種・業界カー用品販売
提供サービスPRゲームコンテンツ
課題・目的交通安全の啓蒙と企業PRにつながる、ゲーム性のあるキャンペーンを実施したい
成果・効果ゲームを通して「かもしれない運転」への理解を促進し、Xでは1000万を超えるインプレッションを獲得

イエローハットの交通安全キャンペーンサイトにおけるPRゲームコンテンツを制作しました。「かもしれない運転」をテーマに、車を操作しながら障害物を避けるシンプルなゲームを実装し、体験を通じて交通安全の大切さを伝える設計としています。

さらに、親しみやすい公式キャラクターを使ったグラフィックと、Xでのフォロー&リポスト施策を実施しました。その結果、SNSキャンペーンでは1,000万を超えるインプレッションを獲得しています。ゲーム体験を通してテーマへの理解を促しながら、SNS施策にも展開し、認知拡大にもつなげた事例です。

事例の詳細についてはこちらからご確認ください。

DOWNLOAD

Webサイト制作サービス資料を無料ダウンロード

フォームに必要事項をご入力いただくと、ご登録のメールアドレス宛に資料のダウンロードURLをお送りします。

DOWNLOAD

未公開実績も多数!
BALANCeの【デザイン・技術】サービス説明資料には、『Javascript』『react』の実績が多数掲載されています。
また、関連した会社資料も無料ダウンロードできますので、ぜひご覧ください!

CONTACT

お見積もりやご提案に関しては、費用は発生いたしません。お気軽に、お問い合わせください。

RECRUIT

BALANCeでは共に働く仲間を随時お待ちしております。
印象に残るプロダクトを一緒に作りませんか?

採用情報はこちら

タグから選ぶ

人気記事

  • 週間

  • 月間

  • 急上昇