mcpField
M
mobile-ux-optimizer

mobile-ux-optimizer

github.com/curiositechデザイン・UI257503 回インストール2 日前に更新
リポジトリ

mobile-ux-optimizer について

mobile-ux-optimizerは、モバイル向けウェブ体験の設計・開発を支援するスキルです。タッチ操作、レスポンシブ対応、性能の指針に加え、ビューポート、安全領域、ナビゲーション、ジェスチャー、実機テストを扱います。

主な機能

  1. 01モバイルファーストのレスポンシブレイアウト
  2. 02ビューポートと安全領域の処理
  3. 03タッチターゲットとモバイルナビゲーション
  4. 04スワイプとプルリフレッシュのジェスチャー
  5. 05モバイル性能の最適化

活用シーン

  1. 01ブラウザーUIによるビューポート切れの修正
  2. 02ノッチやホームインジケーターへの対応
  3. 03モバイルナビゲーションとタッチ操作の設計
  4. 04モバイルの読み込みとスクロールの改善

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill mobile-ux-optimizer

インストール後、Claude Code で/mobile-ux-optimizerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

.claude-plugin/
plugin.json
references/
gestures.md
navigation.md

詳細

モバイルファーストの UX 最適化

適切なビューポート処理とレスポンシブパターンを取り入れ、タッチ操作に最適化された高パフォーマンスのモバイル体験を構築します。

使用する場面

✅ このスキルを使用する場面:

  • ビューポートの問題(100vh の問題、安全領域、ノッチ)
  • タッチターゲットのサイズと間隔
  • モバイルナビゲーションパターン(ボトムナビ、ドロワー、ハンバーガーメニュー)
  • スワイプジェスチャーとプルツーリフレッシュ
  • レスポンシブブレークポイントの戦略
  • モバイルパフォーマンスの最適化

❌ 使用しない場面:

  • ネイティブアプリの開発 → react-native または swift-executor スキルを使用
  • デスクトップ専用の機能 → スキルは不要です。標準的なパターンを使用してください
  • 一般的な CSS/Tailwind の質問 → Tailwind のドキュメントまたは web-design-expert を使用
  • PWA のインストール/サービスワーカー → pwa-expert スキルを使用

基本原則

モバイルファーストとは、縮小するのではなく、段階的に拡張することです
/* ❌ ANTI-PATTERN: Desktop-first (scale down) */
.card { width: 400px; }
@media (max-width: 768px) { .card { width: 100%; } }

/* ✅ CORRECT: Mobile-first (scale up) */
.card { width: 100%; }
@media (min-width: 768px) { .card { width: 400px; } }
44px ルール

Apple の Human Interface Guidelines では、タッチターゲットの最小サイズを 44×44 ポイントと定めています。Google Material では 48×48dp が推奨されています。

// Touch-friendly button
<button className="min-h-[44px] min-w-[44px] px-4 py-3">
  Tap me
</button>

// Touch-friendly link with adequate padding
<a href="/page" className="inline-block py-3 px-4">
  Link text
</a>

ビューポートの処理

dvh による解決策

モバイルブラウザーには動的なツールバーがあります。100vh には URL バーが含まれるため、コンテンツが切れてしまいます。

/* ❌ ANTI-PATTERN: Content hidden behind browser UI */
.full-screen { height: 100vh; }

/* ✅ CORRECT: Responds to browser chrome */
.full-screen { height: 100dvh; }

/* Fallback for older browsers */
.full-screen {
  height: 100vh;
  height: 100dvh;
}
セーフエリアのインセット(ノッチとホームインジケーター)
/* Handle iPhone notch and home indicator */
.bottom-nav {
  padding-bottom: env(safe-area-inset-bottom, 0);
}

.header {
  padding-top: env(safe-area-inset-top, 0);
}

/* Full safe area padding */
.safe-container {
  padding: env(safe-area-inset-top)
           env(safe-area-inset-right)
           env(safe-area-inset-bottom)
           env(safe-area-inset-left);
}

必須の meta タグ:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
Tailwind のセーフエリアクラス
// Custom Tailwind utilities (add to globals.css)
@layer utilities {
  .pb-safe { padding-bottom: env(safe-area-inset-bottom); }
  .pt-safe { padding-top: env(safe-area-inset-top); }
  .h-screen-safe { height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); }
}

// Usage
<nav className="fixed bottom-0 pb-safe bg-leather-900">
  <BottomNav />
</nav>

モバイルナビゲーションパターン

ボトムナビゲーション(モバイルに推奨)
// components/BottomNav.tsx
'use client';

import { usePathname } from 'next/navigation';
import Link from 'next/link';

const navItems = [
  { href: '/', icon: HomeIcon, label: 'Home' },
  { href: '/meetings', icon: CalendarIcon, label: 'Meetings' },
  { href: '/tools', icon: ToolsIcon, label: 'Tools' },
  { href: '/my', icon: UserIcon, label: 'My Recovery' },
];

export function BottomNav() {
  const pathname = usePathname();

  return (
    <nav className="fixed bottom-0 left-0 right-0 bg-leather-900 border-t border-leather-700 pb-safe">
      <div className="flex justify-around">
        {navItems.map(({ href, icon: Icon, label }) => {
          const isActive = pathname === href || pathname.startsWith(`${href}/`);
          return (
            <Link
              key={href}
              href={href}
              className={`
                flex flex-col items-center py-2 px-3 min-h-[56px] min-w-[64px]
                ${isActive ? 'text-ember-400' : 'text-leather-400'}
              `}
            >
              <Icon className="w-6 h-6" />
              <span className="text-xs mt-1">{label}</span>
            </Link>
          );
        })}
      </div>
    </nav>
  );
}
スライドアウトドロワー(サイドメニュー)
'use client';

import { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';

interface DrawerProps {
  isOpen: boolean;
  onClose: () => void;
  children: React.ReactNode;
}

export function Drawer({ isOpen, onClose, children }: DrawerProps) {
  // Prevent body scroll when open
  useEffect(() => {
    if (isOpen) {
      document.body.style.overflow = 'hidden';
    }
    return () => {
      document.body.style.overflow = '';
    };
  }, [isOpen]);

  // Close on escape
  useEffect(() => {
    const handleEscape = (e: KeyboardEvent) => {
      if (e.key === 'Escape') onClose();
    };
    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
  }, [onClose]);

  if (!isOpen) return null;

  return createPortal(
    <div className="fixed inset-0 z-50">
      {/* Backdrop */}
      <div
        className="absolute inset-0 bg-black/60 backdrop-blur-sm"
        onClick={onClose}
        aria-hidden="true"
      />

      {/* Drawer */}
      <div
        className="absolute left-0 top-0 h-full w-[280px] max-w-[80vw]
                   bg-leather-900 shadow-xl transform transition-transform
                   animate-slide-in-left"
        role="dialog"
        aria-modal="true"
      >
        <div className="h-full overflow-y-auto pt-safe pb-safe">
          {children}
        </div>
      </div>
    </div>,
    document.body
  );
}

タッチジェスチャー

実装例の全容は references/gestures.md を参照してください

フック用途
useSwipe()しきい値を設定できる方向別スワイプの検出
usePullToRefresh()視覚的なフィードバックと抵抗感を備えたプルツーリフレッシュ

簡単な使用例:

// Swipe to dismiss
const { handleTouchStart, handleTouchEnd } = useSwipe({
  onSwipeLeft: () => dismiss(),
  threshold: 50,
});

// Pull to refresh
const { containerRef, pullDistance, isRefreshing, handlers } = 
  usePullToRefresh(async () => await refetchData());

モバイルパフォーマンス

画像の最適化
import Image from 'next/image';

// Responsive images with proper sizing
<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, 50vw"
  priority // For above-the-fold images
  className="object-cover"
/>

// Lazy load below-fold images
<Image
  src="/feature.jpg"
  alt="Feature"
  width={400}
  height={300}
  loading="lazy"
/>
バンドルサイズの削減
// Dynamic imports for heavy components
const HeavyChart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Skip server render for client-only
});

// Lazy load below-fold sections
const Comments = dynamic(() => import('@/components/Comments'));
スケルトンスクリーン(スピナーではなく)
// Skeleton that matches final content layout
function MeetingCardSkeleton() {
  return (
    <div className="p-4 bg-leather-800 rounded-lg animate-pulse">
      <div className="h-4 bg-leather-700 rounded w-3/4 mb-2" />
      <div className="h-3 bg-leather-700 rounded w-1/2 mb-4" />
      <div className="flex gap-2">
        <div className="h-6 w-16 bg-leather-700 rounded" />
        <div className="h-6 w-16 bg-leather-700 rounded" />
      </div>
    </div>
  );
}

// Usage
{isLoading ? (
  <div className="space-y-4">
    {[...Array(5)].map((_, i) => <MeetingCardSkeleton key={i} />)}
  </div>
) : (
  meetings.map(m => <MeetingCard key={m.id} meeting={m} />)
)}

レスポンシブパターン

Tailwind のブレークポイント戦略
sm: 640px   - Large phones (landscape)
md: 768px   - Tablets
lg: 1024px  - Small laptops
xl: 1280px  - Desktops
2xl: 1536px - Large screens
// Mobile: stack, Tablet+: side-by-side
<div className="flex flex-col md:flex-row gap-4">
  <aside className="w-full md:w-64">Sidebar</aside>
  <main className="flex-1">Content</main>
</div>

// Mobile: bottom nav, Desktop: sidebar
<nav className="md:hidden fixed bottom-0 left-0 right-0">
  <BottomNav />
</nav>
<aside className="hidden md:block w-64">
  <SidebarNav />
</aside>
コンテナークエリー(CSS のみで実現するレスポンシブコンポーネント)
/* Component responds to its container, not viewport */
@container (min-width: 400px) {
  .card { flex-direction: row; }
}
<div className="@container">
  <div className="flex flex-col @md:flex-row">
    {/* Responds to parent container width */}
  </div>
</div>

実機でのテスト

Chrome DevTools のモバイルエミュレーション
  1. DevTools を開きます(F12)
  2. デバイスツールバーを切り替えます(Ctrl+Shift+M)
  3. デバイスを選択するか、カスタムのサイズを設定します
  4. 現実的なパフォーマンスを再現するため、ネットワーク/CPU をスロットリングします
必ずテストするシナリオ
  • ノッチ/ホームインジケーターでコンテンツが切れない
  • タッチターゲットが 44×44px 以上ある
  • スクロールが滑らかである(カクつかない)
  • ボトムナビがコンテンツを隠さない
  • 仮想キーボードの表示中もフォームが機能する
  • 横向きでも正常に動作する
  • プルツーリフレッシュがスクロールを妨げない
BrowserStack/実機でのテスト
# Expose local dev server to internet
npx localtunnel --port 3000
# or
ngrok http 3000

クイックリファレンス

問題解決策
画面下部でコンテンツが切れる100vh の代わりに 100dvh を使用する
ノッチがコンテンツに重なるpt-safe/pb-safe を追加する
タッチターゲットが小さすぎる最小 44×44px にする
スクロールがロックされるbody の overflow: hidden を確認する
キーボードが入力欄に重なるvisualViewport API を使用する
スクロールがカクつくwill-change: transform を使用する
ダブルタップでズームされるtouch-action: manipulation を追加する

参考資料

詳しいガイドについては /references/ を参照してください:

  • keyboard-handling.md - 仮想キーボードとフォームの UX
  • animations.md - タッチ操作に適したアニメーション
  • accessibility.md - モバイルのアクセシビリティ要件

コメント