T-CREATOR

Motion(旧 Framer Motion)Gesture アニメーション実践:whileHover/whileTap/whileFocus の設計術

Motion(旧 Framer Motion)Gesture アニメーション実践:whileHover/whileTap/whileFocus の設計術

Motion(旧 Framer Motion)Gesture アニメーション実践:whileHover/whileTap/whileFocus の設計術

モダンな Web アプリケーションにおいて、ユーザーとの自然なインタラクションを実現するためには、単なる機能提供だけでなく、直感的で心地よい操作体験が不可欠です。

そこで注目したいのが、Motion(旧 Framer Motion)のジェスチャーアニメーション機能です。whileHover、whileTap、whileFocus という 3 つの強力なプロップスを使いこなすことで、従来の CSS では実現困難だった滑らかで響きのあるユーザー体験を構築できます。

背景

Motion のジェスチャーアニメーション概要

Motion(2024 年に Framer Motion から改名)は、React アプリケーション用のアニメーションライブラリとして、宣言的なアプローチでリッチなインタラクションを実装できる優れたツールです。

特にジェスチャーアニメーションにおいては、ユーザーの操作に対する即座のフィードバックを提供することで、アプリケーションに生命力を吹き込みます。

typescriptimport { motion } from 'motion/react';

// 基本的なジェスチャー対応コンポーネント
const InteractiveButton = () => {
  return (
    <motion.button
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      whileFocus={{ boxShadow: '0 0 0 3px #3b82f6' }}
    >
      クリックしてください
    </motion.button>
  );
};

whileHover、whileTap、whileFocus の基本概念

これら 3 つのプロップスは、それぞれ異なるユーザー操作に反応してアニメーションを実行します。

ジェスチャー発動条件主な用途
whileHoverマウスホバー要素への注目喚起
whileTapタップ・クリック操作フィードバック
whileFocusフォーカス取得アクセシビリティ対応

以下の図は、各ジェスチャーがユーザー操作とどのように連動するかを示しています。

mermaidflowchart LR
  user[ユーザー] -->|マウスオーバー| hover[whileHover]
  user -->|クリック/タップ| tap[whileTap]
  user -->|キーボード操作| focus[whileFocus]

  hover -->|スケール変更| visual1[視覚フィードバック]
  tap -->|色彩変更| visual2[操作フィードバック]
  focus -->|境界線表示| visual3[フォーカス表示]

図で理解できる要点:

  • 各操作は独立したアニメーショントリガーとなる
  • 視覚的フィードバックがユーザー体験を向上させる
  • アクセシビリティにも配慮した設計が可能

インタラクティブ UI における役割

現代の Web アプリケーションでは、静的なインターフェースから動的で応答性の高いインタラクションへとパラダイムシフトが起きています。

Motion のジェスチャーアニメーションは、この変化において中核的な役割を担い、ユーザーの操作意図と画面上の反応を自然に結びつけます。

課題

従来の CSS Hover との違いと制約

従来の CSS :hover 擬似クラスには、いくつかの制約がありました。

css/* 従来のCSS Hover */
.button:hover {
  transform: scale(1.1);
  transition: transform 0.3s ease;
}

この実装では、以下のような問題が発生することがあります。

従来の CSS Hover の制約

  • アニメーション制御の柔軟性が限定的
  • 複雑な状態遷移の実装が困難
  • JavaScript との連携が複雑
  • モバイルデバイスでの一貫性のない動作

一方、Motion の whileHover は、これらの課題を解決します。

typescript// Motion による改善されたホバー実装
<motion.div
  whileHover={{
    scale: 1.1,
    transition: { duration: 0.2, ease: 'easeOut' },
  }}
>
  改善されたホバー体験
</motion.div>

パフォーマンスとアクセシビリティの課題

ジェスチャーアニメーションを実装する際、パフォーマンスとアクセシビリティのバランスを取ることが重要な課題となります。

パフォーマンス課題

  • 大量の要素に適用した場合のメモリ使用量
  • 60FPS を維持するための最適化
  • モバイルデバイスでのバッテリー消費

アクセシビリティ課題

  • 運動機能に制限のあるユーザーへの配慮
  • スクリーンリーダーとの互換性
  • reduce-motion メディアクエリへの対応

複数のジェスチャーを組み合わせる際の問題点

単一のジェスチャーは実装が簡単ですが、複数を組み合わせる際には以下の課題が生じます。

mermaidstateDiagram-v2
  state "デフォルト" as BASE
  [*] --> BASE
  BASE --> hovered : マウスオーバー
  BASE --> focused : キーボードフォーカス
  hovered --> tapped : クリック
  focused --> tapped : Enter/Space
  tapped --> BASE : 操作完了

  note right of tapped : 複数状態の競合が発生する可能性

図で理解できる要点:

  • 状態遷移が複雑になりがち
  • 複数のジェスチャーが同時発生する可能性
  • アニメーション競合の制御が必要

解決策

Motion のジェスチャープロップス設計思想

Motion は、宣言的なアプローチでジェスチャーアニメーションを実装できるよう設計されています。

この設計思想の核心は、「状態」と「アニメーション」を分離し、React の理念に沿った直感的な記述を可能にすることです。

typescript// Motion の宣言的アプローチ
const Component = () => {
  return (
    <motion.div
      // 各状態に対応したアニメーションを宣言的に定義
      animate={{ opacity: 1 }}
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.98 }}
      whileFocus={{
        outline: '2px solid #3b82f6',
        outlineOffset: '2px',
      }}
    >
      宣言的ジェスチャー対応要素
    </motion.div>
  );
};

各ジェスチャーの効果的な活用方法

効果的なジェスチャーアニメーションを実装するためには、各ジェスチャーの特性を理解し、適切な場面で使い分けることが重要です。

whileHover の効果的な活用

  • 要素の発見可能性向上
  • 操作可能であることの示唆
  • ブランドイメージの強化

whileTap の効果的な活用

  • 即座の操作フィードバック
  • ユーザーの操作確信提供
  • タッチデバイスでの触覚的フィードバック代替

whileFocus の効果的な活用

  • キーボードユーザーへの配慮
  • アクセシビリティ基準への準拠
  • 操作フローの視覚的ガイド

アニメーション設計のベストプラクティス

成功するジェスチャーアニメーションには、以下のベストプラクティスを適用します。

1. 適度な動きの設計

typescript// 過度でない、自然な動きを心がける
const subtleAnimation = {
  scale: 1.02, // 大きすぎない拡大
  transition: {
    duration: 0.15, // 短めの持続時間
    ease: 'easeOut', // 自然なイージング
  },
};

2. 一貫性のあるタイミング

アプリケーション全体で統一されたアニメーションタイミングを使用することで、ユーザー体験の統一性を保ちます。

具体例

whileHover の実装とカスタマイズ

基本的なホバーアニメーション

最もシンプルなホバーアニメーションから始めましょう。

typescriptimport { motion } from 'motion/react';

const BasicHoverButton = () => {
  return (
    <motion.button
      className='px-6 py-3 bg-blue-500 text-white rounded-lg'
      whileHover={{
        scale: 1.05,
        backgroundColor: '#3b82f6',
      }}
      transition={{ duration: 0.2 }}
    >
      基本ホバーボタン
    </motion.button>
  );
};

このサンプルでは、ホバー時にボタンが軽くスケールアップし、背景色が変化します。

複雑なホバーエフェクトの実装

より洗練されたホバーエフェクトを実装してみましょう。

typescriptconst AdvancedHoverCard = () => {
  return (
    <motion.div
      className='p-6 bg-white rounded-xl shadow-lg cursor-pointer'
      whileHover={{
        y: -8,
        boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
        transition: { duration: 0.3, ease: 'easeOut' },
      }}
    >
      <motion.div
        whileHover={{ scale: 1.02 }}
        transition={{ duration: 0.2 }}
      >
        <h3 className='text-xl font-bold mb-2'>
          高度なホバーカード
        </h3>
        <p className='text-gray-600'>
          複数のアニメーションを組み合わせた洗練された効果
        </p>
      </motion.div>
    </motion.div>
  );
};

レスポンシブ対応とタッチデバイス配慮

タッチデバイスでは、ホバー状態が存在しないため、適切な配慮が必要です。

typescriptimport { useMediaQuery } from 'react-responsive';

const ResponsiveHoverElement = () => {
  const isTouch = useMediaQuery({
    query: '(pointer: coarse)',
  });

  return (
    <motion.div
      whileHover={
        !isTouch
          ? {
              scale: 1.05,
              transition: { duration: 0.2 },
            }
          : {}
      }
      whileTap={{
        scale: 0.98,
        transition: { duration: 0.1 },
      }}
      className='p-4 bg-gradient-to-r from-purple-500 to-blue-500 text-white rounded-lg'
    >
      レスポンシブ対応要素
    </motion.div>
  );
};

whileTap の実装とカスタマイズ

タップアニメーションの基本実装

タップアニメーションは、ユーザーの操作に対する即座のフィードバックを提供します。

typescriptconst TapFeedbackButton = () => {
  return (
    <motion.button
      className='px-8 py-4 bg-green-500 text-white font-semibold rounded-full'
      whileTap={{
        scale: 0.95,
        backgroundColor: '#10b981',
      }}
      transition={{
        type: 'spring',
        stiffness: 400,
        damping: 17,
      }}
    >
      タップフィードバックボタン
    </motion.button>
  );
};

Spring アニメーションを使用することで、より自然な弾力感のあるフィードバックを実現できます。

フィードバック効果の最適化

効果的なタップフィードバックには、視覚的要素と時間的要素の両方を最適化する必要があります。

typescriptconst OptimizedTapButton = () => {
  return (
    <motion.button
      className='relative px-6 py-3 bg-indigo-600 text-white rounded-lg overflow-hidden'
      whileTap={{
        scale: 0.98,
        transition: { duration: 0.1, ease: 'easeInOut' },
      }}
    >
      {/* リップルエフェクトの実装 */}
      <motion.div
        className='absolute inset-0 bg-white'
        initial={{ scale: 0, opacity: 0.5 }}
        whileTap={{
          scale: 1,
          opacity: 0,
          transition: { duration: 0.4, ease: 'easeOut' },
        }}
        style={{ borderRadius: '50%' }}
      />
      最適化されたタップボタン
    </motion.button>
  );
};

タップとクリックの使い分け

デスクトップとモバイルの両方で一貫した体験を提供するための実装です。

typescriptconst UniversalInteractiveElement = () => {
  const handleInteraction = () => {
    console.log('ユーザーインタラクション発生');
  };

  return (
    <motion.div
      className='p-4 bg-orange-500 text-white rounded-lg cursor-pointer select-none'
      whileTap={{
        scale: 0.97,
        rotate: -1,
      }}
      onTap={handleInteraction}
      onClick={handleInteraction} // フォールバック
    >
      ユニバーサルインタラクション要素
    </motion.div>
  );
};

whileFocus の実装とカスタマイズ

フォーカス状態のアニメーション

キーボードナビゲーションユーザーのためのフォーカス表示を実装します。

typescriptconst FocusableInput = () => {
  return (
    <motion.input
      type='text'
      placeholder='フォーカス対応入力フィールド'
      className='px-4 py-2 border border-gray-300 rounded-lg'
      whileFocus={{
        scale: 1.02,
        borderColor: '#3b82f6',
        boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)',
        transition: { duration: 0.2 },
      }}
    />
  );
};

キーボードナビゲーション対応

Tab キーを使った操作フローでの視認性を向上させます。

typescriptconst NavigationMenu = () => {
  const menuItems = [
    'ホーム',
    '製品',
    '会社概要',
    'お問い合わせ',
  ];

  return (
    <nav className='flex space-x-4'>
      {menuItems.map((item, index) => (
        <motion.a
          key={index}
          href={`#${item}`}
          className='px-3 py-2 text-gray-700 rounded-md focus:outline-none'
          whileFocus={{
            backgroundColor: '#f3f4f6',
            scale: 1.05,
            transition: { duration: 0.15 },
          }}
          tabIndex={0}
        >
          {item}
        </motion.a>
      ))}
    </nav>
  );
};

アクセシビリティ考慮事項

prefers-reduced-motion メディアクエリに対応した実装例です。

typescriptimport { useReducedMotion } from 'motion/react';

const AccessibleFocusElement = () => {
  const shouldReduceMotion = useReducedMotion();

  return (
    <motion.button
      className='px-6 py-3 bg-purple-600 text-white rounded-lg'
      whileFocus={
        shouldReduceMotion
          ? {
              // モーションを減らした控えめなアニメーション
              outline: '2px solid #8b5cf6',
              outlineOffset: '2px',
            }
          : {
              // 通常のアニメーション
              scale: 1.03,
              boxShadow:
                '0 0 0 4px rgba(139, 92, 246, 0.3)',
              transition: { duration: 0.2 },
            }
      }
    >
      アクセシブルなフォーカス要素
    </motion.button>
  );
};

複合ジェスチャーの実装

複数ジェスチャーの組み合わせ技法

すべてのジェスチャーを統合した包括的なインタラクティブコンポーネントを作成します。

typescriptconst ComprehensiveInteractiveCard = () => {
  const [isSelected, setIsSelected] = useState(false);

  return (
    <motion.div
      className={`p-6 rounded-xl cursor-pointer ${
        isSelected
          ? 'bg-blue-100 border-blue-300'
          : 'bg-white border-gray-200'
      } border-2`}
      // ホバー時のアニメーション
      whileHover={{
        y: -4,
        boxShadow: '0 10px 25px -3px rgba(0, 0, 0, 0.1)',
        borderColor: '#3b82f6',
      }}
      // タップ時のアニメーション
      whileTap={{
        scale: 0.98,
        y: -2,
      }}
      // フォーカス時のアニメーション
      whileFocus={{
        outline: '2px solid #3b82f6',
        outlineOffset: '2px',
      }}
      onClick={() => setIsSelected(!isSelected)}
      tabIndex={0}
    >
      <h3 className='text-xl font-bold mb-2'>
        統合インタラクティブカード
      </h3>
      <p className='text-gray-600'>
        ホバー、タップ、フォーカスすべてに対応したカードコンポーネント
      </p>
    </motion.div>
  );
};

状態管理とアニメーション制御

複雑な状態を持つコンポーネントでのアニメーション制御を実装します。

typescriptconst StatefulInteractiveButton = () => {
  const [state, setState] = useState<
    'idle' | 'loading' | 'success'
  >('idle');

  const variants = {
    idle: {
      scale: 1,
      backgroundColor: '#3b82f6',
    },
    loading: {
      scale: 1.05,
      backgroundColor: '#6366f1',
      transition: {
        scale: {
          repeat: Infinity,
          repeatType: 'reverse',
          duration: 0.5,
        },
      },
    },
    success: {
      scale: 1,
      backgroundColor: '#10b981',
    },
  };

  return (
    <motion.button
      className='px-6 py-3 text-white rounded-lg font-medium'
      variants={variants}
      animate={state}
      whileHover={state === 'idle' ? { scale: 1.05 } : {}}
      whileTap={state === 'idle' ? { scale: 0.95 } : {}}
      onClick={() => {
        if (state === 'idle') {
          setState('loading');
          // 2秒後に成功状態に遷移
          setTimeout(() => setState('success'), 2000);
          setTimeout(() => setState('idle'), 3500);
        }
      }}
    >
      {state === 'idle' && '開始'}
      {state === 'loading' && '処理中...'}
      {state === 'success' && '完了!'}
    </motion.button>
  );
};

パフォーマンス最適化

大量の要素や複雑なアニメーションでのパフォーマンス最適化技法です。

typescriptimport { motion, useAnimation } from 'motion/react';
import { useInView } from 'react-intersection-observer';

const OptimizedAnimatedList = ({
  items,
}: {
  items: string[];
}) => {
  return (
    <div className='space-y-2'>
      {items.map((item, index) => (
        <OptimizedListItem key={index} item={item} />
      ))}
    </div>
  );
};

const OptimizedListItem = ({ item }: { item: string }) => {
  const controls = useAnimation();
  const [ref, inView] = useInView({
    threshold: 0.1,
    triggerOnce: true,
  });

  // ビューポートに入った時のみアニメーションを有効化
  React.useEffect(() => {
    if (inView) {
      controls.start('visible');
    }
  }, [controls, inView]);

  return (
    <motion.div
      ref={ref}
      initial='hidden'
      animate={controls}
      variants={{
        hidden: { opacity: 0, y: 20 },
        visible: { opacity: 1, y: 0 },
      }}
      // インビュー時のみジェスチャーアニメーションを有効化
      whileHover={inView ? { x: 10 } : {}}
      whileTap={inView ? { scale: 0.98 } : {}}
      className='p-4 bg-white rounded-lg shadow-sm border cursor-pointer'
    >
      {item}
    </motion.div>
  );
};

以下の図は、パフォーマンス最適化の仕組みを示しています。

mermaidflowchart TD
  viewport[ビューポート] --> intersection[Intersection Observer]
  intersection --> check{要素が見える?}

  check -->|Yes| enable[アニメーション有効化]
  check -->|No| disable[アニメーション無効化]

  enable --> gesture[ジェスチャー検出開始]
  disable --> skip[GPU リソース節約]

  gesture --> animate[スムーズアニメーション]

図で理解できる要点:

  • ビューポート外の要素はアニメーションを無効化
  • 必要な時のみ GPU リソースを使用
  • パフォーマンスとユーザー体験の両立を実現

まとめ

Motion のジェスチャーアニメーション機能(whileHover、whileTap、whileFocus)は、現代的な Web アプリケーションにおいて不可欠な要素となっています。

これらの機能を効果的に活用することで、以下のメリットが得られます。

実装面でのメリット

  • 宣言的で React らしい記述方法
  • CSS transitions よりも柔軟な制御
  • パフォーマンスの最適化された実装

ユーザー体験面でのメリット

  • 直感的で応答性の高いインタラクション
  • アクセシビリティへの適切な配慮
  • デバイスを問わない一貫した操作感

開発効率面でのメリット

  • 複雑な状態管理の簡素化
  • 再利用可能なコンポーネント設計
  • メンテナブルなアニメーション実装

成功の鍵は、過度に複雑にならず、ユーザーの期待に応える自然なアニメーションを心がけることです。また、アクセシビリティやパフォーマンスへの配慮を忘れずに、すべてのユーザーが快適に利用できるインターフェースを構築していきましょう。

Motion のジェスチャーアニメーションを適切に実装することで、単なる機能提供を超えた、ユーザーに愛され続ける Web アプリケーションの開発が可能になります。

関連リンク