Liquid Glass React活用事例:実践的なUI/UXデザインパターン
2025/06/20
8分で読む

Liquid Glass React活用事例:実践的なUI/UXデザインパターン

Liquid Glass効果を活用したReactアプリケーションの実践的な使用例。ナビゲーション、モーダル、カード、フォームなどの具体的な実装例を紹介します。

Liquid Glass効果をReactアプリケーションで効果的に活用するための実践的な使用例を紹介します。実際のプロジェクトで使える具体的なコンポーネントとデザインパターンを詳しく解説します。

ナビゲーション系コンポーネント

固定ヘッダーナビゲーション

スクロール連動型ナビゲーション:

import React, { useState, useEffect } from 'react';
import { LiquidGlass } from './LiquidGlass';
 
const ScrollAwareNavbar = ({ items, logo }) => {
  const [scrolled, setScrolled] = useState(false);
  const [visible, setVisible] = useState(true);
  const [lastScrollY, setLastScrollY] = useState(0);
 
  useEffect(() => {
    const handleScroll = () => {
      const currentScrollY = window.scrollY;
      
      // スクロール状態の更新
      setScrolled(currentScrollY > 50);
      
      // ナビゲーションの表示/非表示制御
      if (currentScrollY > lastScrollY && currentScrollY > 100) {
        setVisible(false); // 下スクロール時は非表示
      } else {
        setVisible(true); // 上スクロール時は表示
      }
      
      setLastScrollY(currentScrollY);
    };
 
    window.addEventListener('scroll', handleScroll, { passive: true });
    return () => window.removeEventListener('scroll', handleScroll);
  }, [lastScrollY]);
 
  return (
    <nav 
      className={`
        fixed top-0 left-0 right-0 z-50 transition-all duration-300
        ${visible ? 'translate-y-0' : '-translate-y-full'}
      `}
    >
      <LiquidGlass 
        intensity={scrolled ? 'strong' : 'medium'}
        className="navbar-container"
      >
        <div className="flex items-center justify-between px-6 py-4">
          <div className="navbar-logo">
            {logo}
          </div>
          
          <ul className="flex space-x-8">
            {items.map((item, index) => (
              <li key={index}>
                <a 
                  href={item.href}
                  className="navbar-link hover:text-blue-600 transition-colors"
                >
                  {item.label}
                </a>
              </li>
            ))}
          </ul>
        </div>
      </LiquidGlass>
    </nav>
  );
};

サイドバーナビゲーション

展開可能なサイドバー:

const LiquidGlassSidebar = ({ isOpen, onToggle, menuItems }) => {
  return (
    <>
      {/* オーバーレイ */}
      {isOpen && (
        <div 
          className="fixed inset-0 bg-black bg-opacity-30 z-40"
          onClick={onToggle}
        />
      )}
      
      {/* サイドバー */}
      <div 
        className={`
          fixed left-0 top-0 h-full w-80 z-50 transform transition-transform duration-300
          ${isOpen ? 'translate-x-0' : '-translate-x-full'}
        `}
      >
        <LiquidGlass intensity="strong" className="h-full">
          <div className="p-6">
            <div className="flex items-center justify-between mb-8">
              <h2 className="text-xl font-semibold">メニュー</h2>
              <button 
                onClick={onToggle}
                className="p-2 hover:bg-white hover:bg-opacity-20 rounded-lg"
              >

              </button>
            </div>
            
            <nav>
              <ul className="space-y-2">
                {menuItems.map((item, index) => (
                  <li key={index}>
                    <a 
                      href={item.href}
                      className="flex items-center p-3 rounded-lg hover:bg-white hover:bg-opacity-20 transition-colors"
                    >
                      <span className="mr-3">{item.icon}</span>
                      {item.label}
                    </a>
                  </li>
                ))}
              </ul>
            </nav>
          </div>
        </LiquidGlass>
      </div>
    </>
  );
};

モーダル・ダイアログ系

多段階モーダル

ステップ形式のモーダル:

const MultiStepModal = ({ isOpen, onClose, steps }) => {
  const [currentStep, setCurrentStep] = useState(0);
  const [formData, setFormData] = useState({});
 
  const nextStep = () => {
    if (currentStep < steps.length - 1) {
      setCurrentStep(currentStep + 1);
    }
  };
 
  const prevStep = () => {
    if (currentStep > 0) {
      setCurrentStep(currentStep - 1);
    }
  };
 
  if (!isOpen) return null;
 
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
      {/* 背景オーバーレイ */}
      <div 
        className="absolute inset-0 bg-black bg-opacity-40"
        onClick={onClose}
      />
      
      {/* モーダルコンテンツ */}
      <LiquidGlass 
        intensity="strong" 
        className="relative w-full max-w-2xl max-h-[90vh] overflow-hidden"
      >
        {/* プログレスバー */}
        <div className="p-6 border-b border-white border-opacity-20">
          <div className="flex items-center justify-between mb-4">
            <h2 className="text-xl font-semibold">
              {steps[currentStep].title}
            </h2>
            <button 
              onClick={onClose}
              className="p-2 hover:bg-white hover:bg-opacity-20 rounded-lg"
            >

            </button>
          </div>
          
          <div className="w-full bg-white bg-opacity-20 rounded-full h-2">
            <div 
              className="bg-blue-500 h-2 rounded-full transition-all duration-300"
              style={{ width: `${((currentStep + 1) / steps.length) * 100}%` }}
            />
          </div>
        </div>
        
        {/* ステップコンテンツ */}
        <div className="p-6 overflow-y-auto">
          {steps[currentStep].content}
        </div>
        
        {/* ナビゲーションボタン */}
        <div className="p-6 border-t border-white border-opacity-20 flex justify-between">
          <button 
            onClick={prevStep}
            disabled={currentStep === 0}
            className="px-4 py-2 bg-white bg-opacity-20 rounded-lg disabled:opacity-50"
          >
            戻る
          </button>
          
          <button 
            onClick={currentStep === steps.length - 1 ? onClose : nextStep}
            className="px-4 py-2 bg-blue-500 text-white rounded-lg"
          >
            {currentStep === steps.length - 1 ? '完了' : '次へ'}
          </button>
        </div>
      </LiquidGlass>
    </div>
  );
};

確認ダイアログ

アクション確認用ダイアログ:

const ConfirmationDialog = ({ 
  isOpen, 
  onClose, 
  onConfirm, 
  title, 
  message, 
  type = 'default' 
}) => {
  const getTypeStyles = (type) => {
    const styles = {
      default: 'border-blue-500 text-blue-600',
      danger: 'border-red-500 text-red-600',
      warning: 'border-yellow-500 text-yellow-600',
      success: 'border-green-500 text-green-600'
    };
    return styles[type] || styles.default;
  };
 
  if (!isOpen) return null;
 
  return (
    <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
      <div 
        className="absolute inset-0 bg-black bg-opacity-40"
        onClick={onClose}
      />
      
      <LiquidGlass 
        intensity="strong" 
        className="relative w-full max-w-md"
      >
        <div className="p-6">
          <div className={`w-12 h-12 rounded-full border-2 ${getTypeStyles(type)} flex items-center justify-center mb-4`}>
            {type === 'danger' && '⚠️'}
            {type === 'warning' && '⚠️'}
            {type === 'success' && '✅'}
            {type === 'default' && 'ℹ️'}
          </div>
          
          <h3 className="text-lg font-semibold mb-2">{title}</h3>
          <p className="text-gray-600 mb-6">{message}</p>
          
          <div className="flex space-x-3">
            <button 
              onClick={onClose}
              className="flex-1 px-4 py-2 bg-white bg-opacity-20 rounded-lg hover:bg-opacity-30 transition-colors"
            >
              キャンセル
            </button>
            <button 
              onClick={() => {
                onConfirm();
                onClose();
              }}
              className={`flex-1 px-4 py-2 rounded-lg text-white ${
                type === 'danger' ? 'bg-red-500 hover:bg-red-600' : 'bg-blue-500 hover:bg-blue-600'
              } transition-colors`}
            >
              確認
            </button>
          </div>
        </div>
      </LiquidGlass>
    </div>
  );
};

カード・コンテンツ系

インタラクティブカードグリッド

ホバー効果付きカードレイアウト:

const InteractiveCardGrid = ({ items }) => {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 p-6">
      {items.map((item, index) => (
        <InteractiveCard key={index} {...item} />
      ))}
    </div>
  );
};
 
const InteractiveCard = ({ title, description, image, tags, onClick }) => {
  const [isHovered, setIsHovered] = useState(false);
 
  return (
    <LiquidGlass 
      intensity={isHovered ? 'strong' : 'medium'}
      className="group cursor-pointer transform transition-all duration-300 hover:scale-105"
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      onClick={onClick}
    >
      {/* 画像部分 */}
      {image && (
        <div className="relative overflow-hidden rounded-t-lg">
          <img 
            src={image} 
            alt={title}
            className="w-full h-48 object-cover transition-transform duration-300 group-hover:scale-110"
          />
          <div className="absolute inset-0 bg-gradient-to-t from-black from-opacity-50 to-transparent" />
        </div>
      )}
      
      {/* コンテンツ部分 */}
      <div className="p-6">
        <h3 className="text-xl font-semibold mb-2 group-hover:text-blue-600 transition-colors">
          {title}
        </h3>
        <p className="text-gray-600 mb-4 line-clamp-3">
          {description}
        </p>
        
        {/* タグ */}
        {tags && (
          <div className="flex flex-wrap gap-2">
            {tags.map((tag, index) => (
              <span 
                key={index}
                className="px-2 py-1 bg-white bg-opacity-20 rounded-full text-sm"
              >
                {tag}
              </span>
            ))}
          </div>
        )}
      </div>
    </LiquidGlass>
  );
};

展開可能なアコーディオン

FAQ形式のアコーディオン:

const LiquidGlassAccordion = ({ items }) => {
  const [openItems, setOpenItems] = useState(new Set());
 
  const toggleItem = (index) => {
    const newOpenItems = new Set(openItems);
    if (newOpenItems.has(index)) {
      newOpenItems.delete(index);
    } else {
      newOpenItems.add(index);
    }
    setOpenItems(newOpenItems);
  };
 
  return (
    <div className="space-y-4">
      {items.map((item, index) => (
        <AccordionItem
          key={index}
          index={index}
          item={item}
          isOpen={openItems.has(index)}
          onToggle={() => toggleItem(index)}
        />
      ))}
    </div>
  );
};
 
const AccordionItem = ({ index, item, isOpen, onToggle }) => {
  return (
    <LiquidGlass 
      intensity="medium"
      className="overflow-hidden transition-all duration-300"
    >
      {/* ヘッダー */}
      <button
        onClick={onToggle}
        className="w-full p-6 text-left flex items-center justify-between hover:bg-white hover:bg-opacity-10 transition-colors"
      >
        <h3 className="text-lg font-semibold">{item.question}</h3>
        <span 
          className={`transform transition-transform duration-300 ${
            isOpen ? 'rotate-180' : 'rotate-0'
          }`}
        >

        </span>
      </button>
      
      {/* コンテンツ */}
      <div 
        className={`overflow-hidden transition-all duration-300 ${
          isOpen ? 'max-h-96 opacity-100' : 'max-h-0 opacity-0'
        }`}
      >
        <div className="px-6 pb-6 border-t border-white border-opacity-20">
          <p className="text-gray-600 mt-4">{item.answer}</p>
        </div>
      </div>
    </LiquidGlass>
  );
};

フォーム系コンポーネント

多段階フォーム

ステップ形式の入力フォーム:

const MultiStepForm = ({ onSubmit }) => {
  const [currentStep, setCurrentStep] = useState(0);
  const [formData, setFormData] = useState({
    personal: {},
    contact: {},
    preferences: {}
  });
 
  const steps = [
    {
      title: '個人情報',
      component: PersonalInfoStep,
      key: 'personal'
    },
    {
      title: '連絡先',
      component: ContactInfoStep,
      key: 'contact'
    },
    {
      title: '設定',
      component: PreferencesStep,
      key: 'preferences'
    }
  ];
 
  const updateFormData = (stepKey, data) => {
    setFormData(prev => ({
      ...prev,
      [stepKey]: { ...prev[stepKey], ...data }
    }));
  };
 
  const nextStep = () => {
    if (currentStep < steps.length - 1) {
      setCurrentStep(currentStep + 1);
    } else {
      onSubmit(formData);
    }
  };
 
  const prevStep = () => {
    if (currentStep > 0) {
      setCurrentStep(currentStep - 1);
    }
  };
 
  const CurrentStepComponent = steps[currentStep].component;
 
  return (
    <div className="max-w-2xl mx-auto p-6">
      <LiquidGlass intensity="medium" className="overflow-hidden">
        {/* プログレスヘッダー */}
        <div className="p-6 border-b border-white border-opacity-20">
          <div className="flex items-center justify-between mb-4">
            {steps.map((step, index) => (
              <div 
                key={index}
                className={`flex items-center ${
                  index <= currentStep ? 'text-blue-600' : 'text-gray-400'
                }`}
              >
                <div 
                  className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
                    index <= currentStep 
                      ? 'border-blue-600 bg-blue-600 text-white' 
                      : 'border-gray-400'
                  }`}
                >
                  {index < currentStep ? '✓' : index + 1}
                </div>
                {index < steps.length - 1 && (
                  <div 
                    className={`w-16 h-0.5 mx-2 ${
                      index < currentStep ? 'bg-blue-600' : 'bg-gray-400'
                    }`}
                  />
                )}
              </div>
            ))}
          </div>
          <h2 className="text-xl font-semibold">{steps[currentStep].title}</h2>
        </div>
 
        {/* フォームコンテンツ */}
        <div className="p-6">
          <CurrentStepComponent
            data={formData[steps[currentStep].key]}
            onChange={(data) => updateFormData(steps[currentStep].key, data)}
          />
        </div>
 
        {/* ナビゲーション */}
        <div className="p-6 border-t border-white border-opacity-20 flex justify-between">
          <button
            onClick={prevStep}
            disabled={currentStep === 0}
            className="px-6 py-2 bg-white bg-opacity-20 rounded-lg disabled:opacity-50 hover:bg-opacity-30 transition-colors"
          >
            戻る
          </button>
          <button
            onClick={nextStep}
            className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
          >
            {currentStep === steps.length - 1 ? '送信' : '次へ'}
          </button>
        </div>
      </LiquidGlass>
    </div>
  );
};

インライン編集フォーム

その場編集可能なフォーム:

const InlineEditForm = ({ initialData, onSave }) => {
  const [isEditing, setIsEditing] = useState(false);
  const [formData, setFormData] = useState(initialData);
  const [hasChanges, setHasChanges] = useState(false);
 
  const handleChange = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    setHasChanges(true);
  };
 
  const handleSave = async () => {
    await onSave(formData);
    setIsEditing(false);
    setHasChanges(false);
  };
 
  const handleCancel = () => {
    setFormData(initialData);
    setIsEditing(false);
    setHasChanges(false);
  };
 
  return (
    <LiquidGlass 
      intensity={isEditing ? 'strong' : 'medium'}
      className="transition-all duration-300"
    >
      <div className="p-6">
        <div className="flex items-center justify-between mb-6">
          <h3 className="text-lg font-semibold">プロフィール情報</h3>
          {!isEditing ? (
            <button
              onClick={() => setIsEditing(true)}
              className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
            >
              編集
            </button>
          ) : (
            <div className="flex space-x-2">
              <button
                onClick={handleCancel}
                className="px-4 py-2 bg-white bg-opacity-20 rounded-lg hover:bg-opacity-30 transition-colors"
              >
                キャンセル
              </button>
              <button
                onClick={handleSave}
                disabled={!hasChanges}
                className="px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 transition-colors"
              >
                保存
              </button>
            </div>
          )}
        </div>
 
        <div className="space-y-4">
          {Object.entries(formData).map(([key, value]) => (
            <div key={key} className="flex items-center space-x-4">
              <label className="w-24 text-sm font-medium capitalize">
                {key}:
              </label>
              {isEditing ? (
                <input
                  type="text"
                  value={value}
                  onChange={(e) => handleChange(key, e.target.value)}
                  className="flex-1 px-3 py-2 bg-white bg-opacity-20 rounded-lg border border-white border-opacity-30 focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              ) : (
                <span className="flex-1 py-2">{value}</span>
              )}
            </div>
          ))}
        </div>
      </div>
    </LiquidGlass>
  );
};

データ表示系

統計ダッシュボード

メトリクス表示カード:

const MetricsCard = ({ title, value, change, trend, icon }) => {
  const getTrendColor = (trend) => {
    return trend === 'up' ? 'text-green-500' : 
           trend === 'down' ? 'text-red-500' : 'text-gray-500';
  };
 
  return (
    <LiquidGlass intensity="medium" className="p-6">
      <div className="flex items-center justify-between mb-4">
        <div className="flex items-center space-x-3">
          <div className="p-2 bg-blue-500 bg-opacity-20 rounded-lg">
            {icon}
          </div>
          <h3 className="text-sm font-medium text-gray-600">{title}</h3>
        </div>
        <span className={`text-sm ${getTrendColor(trend)}`}>
          {trend === 'up' ? '↗' : trend === 'down' ? '↘' : '→'} {change}
        </span>
      </div>
      
      <div className="text-2xl font-bold mb-2">{value}</div>
      
      <div className="w-full bg-white bg-opacity-20 rounded-full h-2">
        <div 
          className="bg-blue-500 h-2 rounded-full transition-all duration-1000"
          style={{ width: `${Math.random() * 100}%` }}
        />
      </div>
    </LiquidGlass>
  );
};
 
const Dashboard = ({ metrics }) => {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 p-6">
      {metrics.map((metric, index) => (
        <MetricsCard key={index} {...metric} />
      ))}
    </div>
  );
};

結論

これらの実用的な使用例は、Liquid Glass効果をReactアプリケーションで効果的に活用する方法を示しています。

重要なポイント:

  • 適切な強度設定: 用途に応じた効果レベルの選択
  • パフォーマンス考慮: 必要以上の効果を避ける
  • アクセシビリティ: 動きの削減オプションの提供
  • ユーザビリティ: 美しさと使いやすさのバランス

これらのコンポーネントを参考に、あなたのプロジェクトに最適なLiquid Glass UIを実装してください。


完全なコンポーネントライブラリとサンプルコードについては、GitHubリポジトリで確認してください。

著者

avatar for macOSTahoe
macOSTahoe

カテゴリ

ニュースレター

コミュニティに参加

最新ニュースとアップデートをお届けするニュースレターに登録しましょう