ブログ一覧に戻る
FrontendReactCSSNext.jsデザイン

キービジュアルをメンテしやすく、かつ格好良くしてみた

はじめに

Niyase のコーポレートサイトのヒーロー (key visual) を全面刷新しました。 4 枚のスライドが順に切り替わって、 モバイル / デスクトップ / クラウド / プラグインの 4 つの価値を伝える構成。 特に 1 枚目は 「実際の業務アプリが 3 ステップで動くアニメーション」 で、 業務統合ソフトという未知のカテゴリでも「あ、 こんな感じね」 と 5 秒で理解させたい狙いがあります。

技術選定の段階で 「画像」「SVG」「React + CSS」 の 3 択で悩んだので、 比較しながら なぜ最終的に React + CSS で組んだか、 そして実装に使ったテクニック 7 つを公開します。

なぜ作り直したか

旧版のヒーローは 3 枚のスライドが全部 抽象的なアイコン配置 + テキスト でした。

[ 9 個の機能アイコン ] [ スペース切替の図解 ] [ 本体 + プラグインの図解 ]

これだとパッと見で 「Niyase は何の会社?」 が伝わりません。 業務統合ソフトという未知のカテゴリに、 無名のブランドが抽象シンボルで挑むのは難易度が高い挑戦でした。 「日本企業もこんな native ぽいアプリ作れるんだ」 という驚きを 5 秒で伝えるには、 実際の製品画面そのものを見せる しかない、 と判断しました。

新版のスライド構成:

  1. モバイル radial パレット (3 ステップで動く)
  2. デスクトップ コマンドパレット候補表示
  3. クラウド スペース切替 (既存維持)
  4. 本体 + プラグイン (標準/独自) のモジュール構成 (既存維持)

ここからが本題です。 「実機の業務アプリ画面を動かす」 1 枚目をどう作るか を掘り下げます。

3 つの選択肢

A. 画像 (PNG / JPG / WebP)

Pros

  • 制作が早い (Figma → エクスポート)
  • ピクセル単位で完璧な見た目
  • ファイルは静的なので軽い (gzip 30〜60KB)

Cons

  • アニメーションが作れない (完全に静的)
  • 製品 UI が変わると 画像も差し替え が必要
  • Retina / Pro Display XDR / 1x で複数バージョン管理
  • ダーク / ライトで 2 倍管理
  • 多言語化で N 倍管理
  • マーケと製品の「キャプチャタイミング合わせ」 が地味に重い

B. SVG イラスト (Figma / Illustrator export)

Pros

  • ベクター = 解像度フリー
  • ファイル軽量 (gzip 5〜15KB)
  • CSS / SMIL で軽くはアニメーションできる
  • フォント差し替えても OK

Cons

  • 複雑な UI モックを描くと 手書き SVG が 500〜1000 行 に膨らむ
  • mask / clip-path / gradient の組み合わせは脆い
  • iPhone のシャドウや frosted glass の再現が苦しい
  • インタラクション (タップ / 状態切替) は結局 JS が必要

C. React + CSS (今回採用)

function SceneMobileRadial() {
  const [step, setStep] = useState<Step>(1);
  // ...3-step state machine
  return (
    <PhoneFrame>
      {step === 3 ? <ExpensesPage /> : <DashboardPage />}
      {step === 2 && <RadialPalette />}
      {tapTarget && <TapRipple target={tapTarget} />}
      <FabButton step={step} />
    </PhoneFrame>
  );
}

Pros

  • 状態切替できる — 「タップで開く / 遷移する」 が表現可能
  • 既存コンポーネントを流用できる — アイコン (lucide-react) / ボタン / カードが製品本体と共通の DSL
  • 製品 UI と乖離しにくい — Tailwind class が製品コードと同じ
  • 解像度 / ダーク・ライト / i18n に 強い (CSS 駆動)
  • diff が読みやすい (PR レビューしやすい)
  • A/B テストや段階リリースに乗せやすい

Cons

  • 初回の制作工数は SVG より重い
  • 「複雑なグラデーションや影」 で CSS の限界に当たることがある
  • パフォーマンス検討要 (transform / opacity 以外を避ける)

比較表

観点画像SVGReact + CSS
制作スピード (初回)★★★★★
メンテ性 (UI 変更追従)★★★★★
アニメーション△ (限定)◯ (柔軟)
状態切替JS 必要ネイティブ
解像度耐性△ (2x/3x 必要)★★★★★★
ダーク/ライト△ (2 倍管理)★★★★★
i18n★★★
ファイルサイズ30〜60KB5〜15KBコード 5〜10KB
デバッガビリティ★★★

トータルで見ると、 スケール時には React + CSS が圧倒的に扱いやすくなります。 「画像で速く」「SVG で軽く」 というショートタームのメリットと、 「動的に正しく」 というロングタームのメリットのトレードオフですが、 サイトを長期運用するなら後者が有利だと判断しました。

採用したスタック

採用したスタックは、 意外とシンプルです。

Next.js 16 App Router
  └─ React Client Components
      ├─ useState / useEffect    (タイマー駆動の state machine)
      ├─ Tailwind CSS            (utility-first)
      ├─ CSS Keyframes           (<style> タグで動的注入)
      ├─ CSS Custom Properties   (per-element パラメータ)
      └─ lucide-react            (アイコン SVG)

外部アニメーションライブラリ (Framer Motion / GSAP / Lottie) は不使用。

理由:

  • 4 スライド × 数百行の CSS で十分賄える
  • ライブラリは bundle が重い (Framer Motion = 約 50KB gzip)
  • CSS keyframe はメインスレッドを介さず合成される (省電力)
  • ライブラリのアップデート追従コストもない

実装で使った 7 つのテクニック

1. CSS Custom Properties で per-instance アニメーション

ラジアル 7 アイテムが扇形に咲く演出です。 各アイテムの 終点座標 が異なるので、 7 個の keyframe を書くのは現実的ではありません。 そこで次のように解決しました。

{
  RADIAL_ITEMS.map((item, i) => {
    const { x, y } = angleToPos(item.angle, RADIAL_RADIUS);
    return (
      <div
        key={item.id}
        className="hero-radial-item-mobile"
        style={
          {
            "--rx": `${x}px`,
            "--ry": `${y}px`,
            animationDelay: `${i * 0.04}s`,
          } as CSSProperties
        }
      >
        ...
      </div>
    );
  });
}
@keyframes hero-radial-bloom-mobile {
  0% {
    opacity: 0;
    transform: translate(0, 0) scale(0.2);
  }
  100% {
    opacity: 1;
    transform: translate(var(--rx), var(--ry)) scale(1);
  }
}

1 つの keyframe を全アイテムで共有します。 per-instance のパラメータは CSS custom property で渡します。 これだけで「7 個が時間差で別の場所に咲く」 が実現できます。

2. State Machine でステップ駆動

3 ステップ (初期 → パレット展開 → 遷移後) を タイマーチェーン で循環させます。

useEffect(() => {
  setTapTarget(null);
  const timers: ReturnType<typeof setTimeout>[] = [];

  // STEP 末 420ms 前にタップ波紋を発火
  if (step === 1) timers.push(setTimeout(() => setTapTarget("fab"), 2580));
  if (step === 2) timers.push(setTimeout(() => setTapTarget("receipt"), 2580));

  // STEP 自体の遷移
  timers.push(
    setTimeout(() => {
      setStep((s) => ((s % 3) + 1) as Step);
    }, STEP_DURATIONS[step]),
  );

  return () => timers.forEach(clearTimeout);
}, [step]);

「タップ波紋 → 0.4 秒後に次の状態へ」 がきれいに連動します。 タップしている感が出るので、 「ユーザーが操作している → 結果として画面が変わる」 という因果関係が視覚的に伝わります。

3. key prop による強制 remount で keyframe を再発火

CSS keyframe は要素 mount 時に 1 度だけ走ります。 同じ要素で再生したい時は、 key を変えて unmount/remount させます。

<div key={step === 3 ? "destination" : "home"} className="hero-page-crossfade">
  {step === 3 ? <ExpensesPage /> : <DashboardPage />}
</div>

step が 2→3 になると key が変わって remount され、 hero-page-crossfade の fade-in が再生されます。 Framer Motion の AnimatePresence 相当のことが、 React の組み込み機能だけで実現できます。

4. レイヤー分離で transform 衝突を回避

「位置決めの translate」 と 「タップ press の scale」 を 同じ要素 に乗せると、 後者が前者を上書きします。 解決策はシンプルで、 3 層に分けます。

{/* レイヤー 1: bloom の translate */}
<div className="hero-radial-item-mobile" style={{"--rx": ..., "--ry": ...}}>
  {/* レイヤー 2: 中心寄せ */}
  <div style={{ transform: "translate(-50%, -50%)" }}>
    {/* レイヤー 3: タップ押下 scale */}
    <div className={isTapped ? "hero-tap-press" : undefined}>
      {/* 実体 */}
      <div className="rounded-full bg-emerald-500 ...">...</div>
    </div>
  </div>
</div>

各レイヤーが 1 つの transform に専念するので、 keyframe 同士が干渉しません。 「DOM を一段増やすと遅くなる」 という昔のドグマもありますが、 GPU レイヤー数が増えても modern browser では誤差の範囲です。 見通しのよさとメンテ性のほうが大きく上回ります。

5. 多層 box-shadow でガラス感を出す

iPhone モックの「浮遊感」 は単一 shadow ではなく、 複数 shadow を重ねて表現します

box-shadow:
  0 50px 100px -20px rgba(52, 211, 153, 0.45),
  /* メインの落ち影 + emerald glow */ 0 0 0 1px rgba(255, 255, 255, 0.08),
  /* 外周の薄い枠 */ inset 0 0 0 1px rgba(255, 255, 255, 0.05); /* 内側の縁取り */

3 つ混ぜると 金属筐体感 + emerald 発光 + ガラスの縁取り がまとめて得られます。 SVG では <filter> primitive を組み合わせる必要がある部分も、 CSS なら 1 行ずつ重ねるだけで済みます。 これだけで質感が一段上がります。

6. タップ波紋エフェクト

タップしている瞬間の演出は、 「同心円が広がるリング × 2」 + 「中央のドット」 + 「タップ対象の press scale」 の 3 つで構成します。

@keyframes hero-tap-pulse {
  0% {
    opacity: 0.85;
    transform: scale(0.25);
  }
  100% {
    opacity: 0;
    transform: scale(1.6);
  }
}

@keyframes hero-tap-press {
  0% {
    transform: scale(1);
  }
  35% {
    transform: scale(0.86);
  }
  70% {
    transform: scale(1.02);
  }
  100% {
    transform: scale(1);
  }
}

リングは 120ms の時間差で 2 発、 中央ドットは scale 0.5→1→0.7 でフラッシュし、 押下対象は overshoot 入りの scale で縮んで戻ります。 合計 0.5 秒の演出で、 ユーザーが画面を触っている感覚が伝わります。

7. 純 React のタイピング演出

ヘッドラインを 1 文字ずつタイプする演出も、 React と setTimeout だけで実現できます。

useEffect(() => {
  if (phase !== "typing") return;
  const delay = charIndex === 0 ? START_DELAY : TYPING_SPEED;
  const timer = setTimeout(() => {
    if (charIndex < totalLength) setCharIndex((prev) => prev + 1);
    else setPhase("display");
  }, delay);
  return () => clearTimeout(timer);
}, [phase, charIndex, totalLength]);

string.slice(0, charIndex) で部分文字列を render します。 「Typewriter ライブラリ」 を導入する必要はありません。

結果: メンテと拡張がラク

ヒーローの copy 変更は 1 行の diff で完了します。

- white: "メニューを覚えない、",
- accent: "AI が候補を提示。",
+ white: "覚えなくていい、",
+ accent: "AI が次の一手を提案。",

もし画像 / SVG で作っていたら、 デザイナーに依頼 → 修正 → エクスポート → 差し替え → 解像度別書き出しの再生成、 と最低 2 日はかかっていたはずです。

新スライドを足すのも簡単です。

  • heroMessagesNew.ts に 1 オブジェクト追加
  • HeroScenesNew.tsx にシーン関数 1 つ追加
  • HeroSectionNew.tsx の switcher に 1 行追加

体感 1 時間で 1 スライド 増やせる構造になっています。

正直なトレードオフ

観点不利な点
初回制作時間画像なら 2 時間、 これは 1.5 日
デザイナー単独で触りにくいTailwind / CSS 知識が必要
ピクセル完璧主義との相性font rendering の OS 差は完全には吸収できない
「実機録画動画」 には勝てない説得力では動画が別格

なので、 「製品 UI と長く併走するキービジュアル」 にはこの方式、 「打ち切り前提のキャンペーンビジュアル」 には画像 / 動画、 と棲み分けるのが良いと考えています。 用途に応じて道具を変えるということです。

まとめ

  • ヒーローを 「画像 / SVG / React + CSS」 の 3 択で検討
  • 「製品 UI と乖離させない」「長期メンテしたい」 観点で React + CSS を採用
  • 外部アニメライブラリ無し、 純 CSS keyframe + React state machine で動かせる
  • 主要テクニック:
    • CSS Custom Properties で per-instance パラメータ
    • State Machine + タイマーチェーン で多段アニメ
    • key prop remount で keyframe 再発火
    • レイヤー分離 で transform 衝突回避
    • 多層 box-shadow でガラス感
    • タップ波紋 で操作感を表現
    • 純 React のタイピング演出
  • 文言修正は 1 行差分で完結、 PR レビューもしやすい

「キービジュアルを画像で逃げてきたけど運用が辛い」 という方は、 試してみてください。 1 度組むと、 マーケと製品の境界が地続きになるので、 結果的にチームの開発速度が上がります。

Niyase コーポレートサイトの新ヒーローは niyase.com で動いています。 ぜひ実際の動きを確認してみてください。