mobile-ux-optimizer について
mobile-ux-optimizerは、モバイル向けウェブ体験の設計・開発を支援するスキルです。タッチ操作、レスポンシブ対応、性能の指針に加え、ビューポート、安全領域、ナビゲーション、ジェスチャー、実機テストを扱います。
主な機能
- 01モバイルファーストのレスポンシブレイアウト
- 02ビューポートと安全領域の処理
- 03タッチターゲットとモバイルナビゲーション
- 04スワイプとプルリフレッシュのジェスチャー
- 05モバイル性能の最適化
活用シーン
- 01ブラウザーUIによるビューポート切れの修正
- 02ノッチやホームインジケーターへの対応
- 03モバイルナビゲーションとタッチ操作の設計
- 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.jsonreferences/gestures.mdnavigation.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 のモバイルエミュレーション
- DevTools を開きます(F12)
- デバイスツールバーを切り替えます(Ctrl+Shift+M)
- デバイスを選択するか、カスタムのサイズを設定します
- 現実的なパフォーマンスを再現するため、ネットワーク/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- 仮想キーボードとフォームの UXanimations.md- タッチ操作に適したアニメーションaccessibility.md- モバイルのアクセシビリティ要件
コメント
Markdown 対応