👊

折りたたみアニメーションを作れるようになろう

に公開

CSSなんてほぼしらない状態でtailwindcssを使い続けてはや半年。アニメーションをつけたいときもframer-motionでなんとかなるし、正直フロントはNextだったら普通のCSSかけなくてもやっていけるでしょと甘い考えで生きてきました。
が、メールフォームを作成していたとき、送信するときに手紙を折って羽で飛んでいくアニメーションが作りたくなってClaudeに助けを求めたところ3Dアニメーションはframer-motionよりネイティブのCSSのほうが書きやすいで。と言われたのでClaudeに書いてもらったコードを見ながら学ぼうと思います。

初学者なので間違い等ありましたら優しく教えていただけるとありがたいです。。。

今回見ていくコード

oritatami.tsx
// クライアントのみで動くということを宣言。これをつけないとuseStateなどのhooksと呼ばれるものは使えない。
// 逆にこれをつけることでサーバー側に通信が必要な処理(API Routeやcookie, supabase)は行えない
"use client";

// React本体と状態管理に使うuseStateを読み込む
import React, { useState } from "react";

// FoldingCard という名前の関数コンポーネントを定義
const FoldingCard = () => {
  // isFolded という state(状態)を宣言。初期値はfalse(折りたたまれていない=開いた状態)
  // setIsFolded はその state を更新する関数。
  const [isFolded, setIsFolded] = useState(false);

  // 折りたたむ / 開く動作を切り替える関数を定義。
  const toggleFold = () => {
    // 現在の isFolded の真偽値をひっくり返して反映。
    setIsFolded(!isFolded);
  };

  return (
    // ここからは画面に描画する部分。
    // 画面いっぱいの高さを取る / フレックスコンテナにする(横) / 子要素を中央に配置 / パディングは 2rem(p-8)。
    <div className="min-h-screen flex items-center justify-center p-8">
      {/* relative を指定して子要素の絶対配置を可能にする。 / 3D空間を設定して、要素と視点の距離を1200pxにする。 */}
      <div className="relative" style={{ perspective: "1200px" }}>
        {/* 絶対配置 / top, right, bottom, leftを0にする / width / height / ホバーしたときのカーソルをポインターにする / z軸を50にする(100になればなるほど要素が上にくる) */}
        {/* クリックしたときにtoggleFoldという関数を起動する */}
        <div
          className="absolute inset-0 w-80 h-96 cursor-pointer z-50"
          onClick={toggleFold}
        />

        {/* メインのカード。子要素も含めたネスト全体が3D空間として扱われるようにする。 */}
        <div
          className="relative w-80 h-96"
          style={{ transformStyle: "preserve-3d" }}
        >
          {/* rounded-t-xl: 要素の上側(top)を少し強めに(xl)丸くする / shadow-xl: 影を少し強めにつける / はみ出した部分を隠す */}
          <div
            className="absolute top-0 left-0 w-full h-48 rounded-t-xl shadow-xl overflow-hidden"
            style={{
              transformOrigin: "bottom center", // 回転の軸を下辺中央に設定
              transform: isFolded // isFolded が true なら要素をx軸まわりに180度(degree)傾ける、false なら元の向き
                ? "rotateX(-180deg)"
                : "rotateX(0deg)",
              // transformの変化を0.8秒かけて滑らかにアニメーション(ベジェ曲線のx1, y1, x2, y2)
              // cubiv-bezierについてはページ下部に簡単な考え方?を記載しています。
              transition: "transform 0.8s cubic-bezier(0, 0.1, 0, 0.1)",
              transformStyle: "preserve-3d", // 3D 効果を子要素にも適用
              zIndex: 10, // z軸を10に設定
            }}
          >
            {/* カード上半分(折りたたむ部分)の開始 */}
            {/* backgraundをblueの濃さ500 */}
            <div
              className="absolute inset-0 w-full h-full bg-blue-500"
              style={{
                backfaceVisibility: "hidden", // 裏側(背面)が見えないようにする。見えないだけで当たり判定はある。
              }}
            >
              {/* 折りたたまれていないときに見える「表面」 */}
              {/* flex-col: フレックスを縦にする /  */}
              <div className="p-6 h-full flex flex-col justify-center items-center text-white">
                {/* mb: margin-bottom */}
                <div className="text-4xl mb-3">📄</div>
                <h2 className="text-xl font-bold">カード内容</h2>
              </div>
            </div>

            <div
              className="absolute inset-0 w-full h-full bg-gray-700"
              style={{
                // 初期状態で 180°ひっくり返して裏側に配置しておく
                backfaceVisibility: "hidden",
                transform: "rotateX(180deg)",
              }}
            >
              {/* 折りたたむと見える「裏面」 */}
              <div className="p-6 h-full flex flex-col justify-center items-center text-white">
                <div className="text-4xl mb-3">📖</div>
                <h3 className="text-lg font-bold">表紙</h3>
              </div>
            </div>
          </div>

          {/* カード下半分 */}
          <div
            className="absolute bottom-0 left-0 w-full h-48 bg-purple-500 rounded-b-xl shadow-xl overflow-hidden"
            // z軸を5にして"カード内容"の部分(z=10)より下げることで折り畳まれたときに"カード内容"の裏面が上にくるようにする
            style={{ zIndex: 5 }}
          >
            <div className="p-6 h-full flex flex-col justify-center items-center text-white">
              <div className="text-4xl mb-3"></div>
              <h3 className="text-lg font-bold">固定部分</h3>
            </div>
          </div>

          {/* カード中央の境界線(折り目ライン) */}
          <div
            className="absolute left-0 right-0 top-1/2 h-0.5 bg-black/30 pointer-events-none"
            style={{
              transform: "translateY(-50%)", //top-1/2だと要素の上端を中央にもってくるだけなのでそこからまた上(Y-50%)をすることで要素の中心を中央に移動させている
              opacity: isFolded ? 0 : 1, // 折りたたむとラインを消す
              transition: "opacity 0.4s ease",
              zIndex: 20,
            }}
          />
        </div>
      </div>
    </div>
  );
};

export default FoldingCard;


補足説明

途中で登場するベジェ曲線についてはYouTubeの解説動画を参考にしたものの、まだ十分に理解できていません。
が、簡単に以下のように考えています。
cubic-bezier(x1, y1, x2, y2)
このx1, y1とx2, y2のそれぞれの値が近ければ近いほどゆっくり動き、遠ければ遠いほど早く。
cubic-bezier(0, 0.1, 0, 0.1) //ゆっくり
cubic-bezier(0, 1, 0, 1) //はやい
またx1, y1が前半、x2, y2が後半の速度と思えばわかりやすいかなと思います。
cubic-bezier(0, 0.1, 0, 1) //最初はゆっくり、最後ははやい
cubic-bezier(0, 1, 0, 0.1) //最初ははやく、最後はゆっくり

カードは合計で3枚配置してあり下記の画像のようになっています。

Discussion