ui-mobile
モバイルUIパターンに関するスキルで、React Native・iOS/Android向けの実装やタッチターゲットの設計に対応します。モバイルアプリのUI構築・最適化が必要な場面で活用できます。
description の原文を見る
Mobile UI patterns - React Native, iOS/Android, touch targets
SKILL.md 本文
モバイル UI デザイン スキル (React Native)
必須: モバイル アクセシビリティ標準
これらのルールは譲歩の余地がありません。すべての UI 要素がこれらのチェックに合格する必要があります。
1. タッチターゲット (重大)
// すべてのインタラクティブ要素に対して最小 44x44 ポイント
const MINIMUM_TOUCH_SIZE = 44;
// すべてのボタン、リンク、アイコンボタンがこれを満たす必要があります
const styles = StyleSheet.create({
button: {
minHeight: MINIMUM_TOUCH_SIZE,
minWidth: MINIMUM_TOUCH_SIZE,
paddingVertical: 12,
paddingHorizontal: 16,
},
iconButton: {
width: MINIMUM_TOUCH_SIZE,
height: MINIMUM_TOUCH_SIZE,
justifyContent: 'center',
alignItems: 'center',
},
});
// これはしないでください:
style={{ height: 30 }} // ✗ 小さすぎる
style={{ padding: 4 }} // ✗ 極小のターゲットになります
2. 色のコントラスト (重大)
// WCAG 2.1 AA: テキストは 4.5:1、大きいテキスト/UI は 3:1
// 安全な組み合わせ:
const colors = {
// ライトモード
textPrimary: '#000000', // 白背景で 21:1 ✓
textSecondary: '#374151', // gray-700 白背景で 9.2:1 ✓
// ダークモード
textPrimaryDark: '#FFFFFF', // gray-900 背景で 16:1 ✓
textSecondaryDark: '#E5E7EB', // gray-200 gray-900 背景で 11:1 ✓
};
// 禁止 - コントラスト不足:
// ✗ '#9CA3AF' (gray-400) 白背景で = 2.6:1
// ✗ '#6B7280' (gray-500) '#111827' 背景で = 4.0:1
// ✗ 4.5:1 未満のテキスト
3. 見視性ルール
// すべてのボタンは視認可能な境界を持つ必要があります
// プライマリ: 背景色とコントラストのあるテキスト
<Pressable style={styles.primaryButton}>
<Text style={{ color: '#FFFFFF' }}>Submit</Text>
</Pressable>
const styles = StyleSheet.create({
primaryButton: {
backgroundColor: '#1F2937', // gray-800
paddingVertical: 16,
paddingHorizontal: 24,
borderRadius: 12,
minHeight: 44,
},
});
// セカンダリ: 視認可能な背景
<Pressable style={styles.secondaryButton}>
<Text style={{ color: '#1F2937' }}>Cancel</Text>
</Pressable>
const styles = StyleSheet.create({
secondaryButton: {
backgroundColor: '#F3F4F6', // gray-100
minHeight: 44,
},
});
// ゴースト: 視認可能なボーダーが必須
<Pressable style={styles.ghostButton}>
<Text style={{ color: '#374151' }}>Skip</Text>
</Pressable>
const styles = StyleSheet.create({
ghostButton: {
borderWidth: 1,
borderColor: '#D1D5DB', // gray-300
minHeight: 44,
},
});
// 見えないボタンを作成しないでください:
// ✗ ボーダーなしの backgroundColor: 'transparent'
// ✗ 背景と同じ色のテキスト
4. アクセシビリティ ラベル (必須)
// すべてのインタラクティブ要素にアクセシビリティプロパティが必要です
// ボタン
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="Submit form"
accessibilityHint="Double tap to submit your information"
>
<Text>Submit</Text>
</Pressable>
// アイコンボタン (目に見えるテキストなし = ラベルが必須)
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="Close menu"
>
<CloseIcon />
</Pressable>
// 画像
<Image
accessible={true}
accessibilityRole="image"
accessibilityLabel="User profile photo"
source={...}
/>
5. フォーカス/選択状態
// すべての Pressable に見える押下状態が必要です
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
>
{children}
</Pressable>
const styles = StyleSheet.create({
button: {
backgroundColor: '#1F2937',
},
buttonPressed: {
opacity: 0.7,
// または
backgroundColor: '#374151',
},
});
コア哲学
モバイル UI はタッチ、速度、フォーカスについてです。 ホバー状態はなく、画面は小さく、親指に優しいターゲット。片手操作と中断からの復旧を想定して設計します。
プラットフォーム差
iOS vs Android
import { Platform } from 'react-native';
// プラットフォーム固有の値
const styles = StyleSheet.create({
shadow: Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
android: {
elevation: 4,
},
}),
// iOS は SF Pro、Android は Roboto を使用
text: {
fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
},
});
デザイン言語
iOS (ヒューマン インターフェイス ガイドライン)
─────────────────────────────────
- 微妙な奥行きを持つフラットデザイン
- SF Symbols アイコン
- 大きなタイトル (34pt)
- 角丸 (10-14pt)
- デフォルトの色合いは青
Android (Material Design 3)
─────────────────────────────────
- Material You ダイナミックカラー
- アウトライン/フィルド アイコン
- 中サイズのタイトル (22pt)
- 角丸 (12-28pt)
- テーマからのプライマリ色
スペーシング システム
4px ベース グリッド
// React Native スペーシング - 一貫性のあるスケール
const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
'2xl': 48,
} as const;
// 使用例
const styles = StyleSheet.create({
container: {
padding: spacing.md,
gap: spacing.sm,
},
});
セーフ エリア
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const Screen = ({ children }) => {
const insets = useSafeAreaInsets();
return (
<View style={{
flex: 1,
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: Math.max(insets.left, 16),
paddingRight: Math.max(insets.right, 16),
}}>
{children}
</View>
);
};
タイポグラフィ
タイプ スケール
const typography = {
// 大きなタイトル (iOS スタイル)
largeTitle: {
fontSize: 34,
fontWeight: '700' as const,
letterSpacing: 0.37,
},
// セクション ヘッダー
title: {
fontSize: 22,
fontWeight: '700' as const,
letterSpacing: 0.35,
},
// カード タイトル
headline: {
fontSize: 17,
fontWeight: '600' as const,
letterSpacing: -0.41,
},
// 本文
body: {
fontSize: 17,
fontWeight: '400' as const,
letterSpacing: -0.41,
lineHeight: 22,
},
// セカンダリ テキスト
callout: {
fontSize: 16,
fontWeight: '400' as const,
letterSpacing: -0.32,
},
// 小さいラベル
caption: {
fontSize: 12,
fontWeight: '400' as const,
letterSpacing: 0,
},
};
カラー システム
セマンティック カラー
// リテラル色ではなくセマンティック名を使用します
const colors = {
// 背景
background: '#FFFFFF',
backgroundSecondary: '#F2F2F7',
backgroundTertiary: '#FFFFFF',
// サーフェス
surface: '#FFFFFF',
surfaceElevated: '#FFFFFF',
// テキスト
label: '#000000',
labelSecondary: '#3C3C43', // 60% 不透明度
labelTertiary: '#3C3C43', // 30% 不透明度
// アクション
primary: '#007AFF',
destructive: '#FF3B30',
success: '#34C759',
warning: '#FF9500',
// セパレーター
separator: '#3C3C43', // 29% 不透明度
opaqueSeparator: '#C6C6C8',
};
// ダークモード バリアント
const darkColors = {
background: '#000000',
backgroundSecondary: '#1C1C1E',
label: '#FFFFFF',
labelSecondary: '#EBEBF5', // 60% 不透明度
separator: '#545458',
};
ダイナミック カラー (React Native)
import { useColorScheme } from 'react-native';
const useColors = () => {
const scheme = useColorScheme();
return scheme === 'dark' ? darkColors : colors;
};
// 使用例
const MyComponent = () => {
const colors = useColors();
return (
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.label }}>Hello</Text>
</View>
);
};
タッチ ターゲット
最小サイズ
// 重大: 最小 44pt タッチターゲット
const touchable = {
minHeight: 44,
minWidth: 44,
};
// 適切なサイジングのボタン
const styles = StyleSheet.create({
button: {
minHeight: 44,
paddingHorizontal: 16,
paddingVertical: 12,
justifyContent: 'center',
alignItems: 'center',
},
// アイコン ボタン (正方形)
iconButton: {
width: 44,
height: 44,
justifyContent: 'center',
alignItems: 'center',
},
// リスト行
listRow: {
minHeight: 44,
paddingVertical: 12,
paddingHorizontal: 16,
},
});
タッチ フィードバック
import { Pressable } from 'react-native';
// iOS スタイルの不透明度フィードバック
const Button = ({ children, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.button,
pressed && { opacity: 0.7 },
]}
>
{children}
</Pressable>
);
// Android スタイルのリップル
const AndroidButton = ({ children, onPress }) => (
<Pressable
onPress={onPress}
android_ripple={{
color: 'rgba(0, 0, 0, 0.1)',
borderless: false,
}}
style={styles.button}
>
{children}
</Pressable>
);
コンポーネント パターン
カード
const Card = ({ children, style }) => (
<View style={[styles.card, style]}>
{children}
</View>
);
const styles = StyleSheet.create({
card: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 8,
},
android: {
elevation: 2,
},
}),
},
});
ボタン
// プライマリ ボタン
const PrimaryButton = ({ title, onPress, disabled }) => (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.primaryButton,
pressed && styles.primaryButtonPressed,
disabled && styles.buttonDisabled,
]}
>
<Text style={styles.primaryButtonText}>{title}</Text>
</Pressable>
);
const styles = StyleSheet.create({
primaryButton: {
backgroundColor: '#007AFF',
borderRadius: 12,
paddingVertical: 16,
paddingHorizontal: 24,
alignItems: 'center',
},
primaryButtonPressed: {
backgroundColor: '#0056B3',
},
primaryButtonText: {
color: '#FFFFFF',
fontSize: 17,
fontWeight: '600',
},
buttonDisabled: {
opacity: 0.5,
},
});
// セカンダリ ボタン
const SecondaryButton = ({ title, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.secondaryButton,
pressed && { opacity: 0.7 },
]}
>
<Text style={styles.secondaryButtonText}>{title}</Text>
</Pressable>
);
入力フィールド
const TextField = ({ label, value, onChangeText, error }) => {
const [focused, setFocused] = useState(false);
return (
<View style={styles.textFieldContainer}>
{label && (
<Text style={styles.textFieldLabel}>{label}</Text>
)}
<TextInput
value={value}
onChangeText={onChangeText}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
style={[
styles.textField,
focused && styles.textFieldFocused,
error && styles.textFieldError,
]}
placeholderTextColor="#8E8E93"
/>
{error && (
<Text style={styles.errorText}>{error}</Text>
)}
</View>
);
};
const styles = StyleSheet.create({
textFieldContainer: {
gap: 8,
},
textFieldLabel: {
fontSize: 15,
fontWeight: '500',
color: '#3C3C43',
},
textField: {
backgroundColor: '#F2F2F7',
borderRadius: 10,
paddingHorizontal: 16,
paddingVertical: 14,
fontSize: 17,
color: '#000000',
borderWidth: 2,
borderColor: 'transparent',
},
textFieldFocused: {
borderColor: '#007AFF',
backgroundColor: '#FFFFFF',
},
textFieldError: {
borderColor: '#FF3B30',
},
errorText: {
fontSize: 13,
color: '#FF3B30',
},
});
リスト
// グループ化されたリスト (iOS 設定スタイル)
const GroupedList = ({ sections }) => (
<ScrollView style={styles.groupedList}>
{sections.map((section, i) => (
<View key={i} style={styles.section}>
{section.title && (
<Text style={styles.sectionHeader}>{section.title}</Text>
)}
<View style={styles.sectionContent}>
{section.items.map((item, j) => (
<React.Fragment key={j}>
{j > 0 && <View style={styles.separator} />}
<Pressable
style={({ pressed }) => [
styles.listRow,
pressed && { backgroundColor: '#E5E5EA' },
]}
onPress={item.onPress}
>
<Text style={styles.listRowText}>{item.title}</Text>
<ChevronRight color="#C7C7CC" />
</Pressable>
</React.Fragment>
))}
</View>
</View>
))}
</ScrollView>
);
const styles = StyleSheet.create({
groupedList: {
flex: 1,
backgroundColor: '#F2F2F7',
},
section: {
marginTop: 35,
},
sectionHeader: {
fontSize: 13,
fontWeight: '400',
color: '#6D6D72',
textTransform: 'uppercase',
marginLeft: 16,
marginBottom: 8,
},
sectionContent: {
backgroundColor: '#FFFFFF',
borderRadius: 10,
marginHorizontal: 16,
overflow: 'hidden',
},
listRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
paddingHorizontal: 16,
minHeight: 44,
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#C6C6C8',
marginLeft: 16,
},
});
ナビゲーション パターン
ボトム タブ バー
// 適切なボトムタブサイズ
const tabBarStyle = {
height: Platform.OS === 'ios' ? 83 : 65, // ホームインジケーターを考慮
paddingBottom: Platform.OS === 'ios' ? 34 : 10,
paddingTop: 10,
backgroundColor: '#F8F8F8',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#C6C6C8',
};
// タブアイテム
const TabItem = ({ icon, label, active }) => (
<View style={styles.tabItem}>
<Icon name={icon} color={active ? '#007AFF' : '#8E8E93'} size={24} />
<Text style={[
styles.tabLabel,
{ color: active ? '#007AFF' : '#8E8E93' }
]}>
{label}
</Text>
</View>
);
ヘッダー
// 大きなタイトル ヘッダー (iOS)
const LargeTitleHeader = ({ title, rightAction }) => {
const insets = useSafeAreaInsets();
return (
<View style={[styles.header, { paddingTop: insets.top }]}>
<View style={styles.headerContent}>
<Text style={styles.largeTitle}>{title}</Text>
{rightAction}
</View>
</View>
);
};
const styles = StyleSheet.create({
header: {
backgroundColor: '#F8F8F8',
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: '#C6C6C8',
},
headerContent: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingBottom: 8,
},
largeTitle: {
fontSize: 34,
fontWeight: '700',
letterSpacing: 0.37,
},
});
アニメーション
ネイティブ ドライバー アニメーション
import { Animated } from 'react-native';
// 可能な限りネイティブ ドライバーを使用します
const fadeIn = (value: Animated.Value) => {
Animated.timing(value, {
toValue: 1,
duration: 200,
useNativeDriver: true, // パフォーマンスに重大
}).start();
};
// 自然な感じのスプリング
const bounce = (value: Animated.Value) => {
Animated.spring(value, {
toValue: 1,
damping: 15,
stiffness: 150,
useNativeDriver: true,
}).start();
};
複雑なアニメーション用 Reanimated
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
const AnimatedCard = ({ children }) => {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const onPressIn = () => {
scale.value = withSpring(0.95);
};
const onPressOut = () => {
scale.value = withSpring(1);
};
return (
<Pressable onPressIn={onPressIn} onPressOut={onPressOut}>
<Animated.View style={[styles.card, animatedStyle]}>
{children}
</Animated.View>
</Pressable>
);
};
ローディング状態
スケルトン ローダー
const SkeletonLoader = ({ width, height, borderRadius = 4 }) => {
const opacity = useSharedValue(0.3);
useEffect(() => {
opacity.value = withRepeat(
withSequence(
withTiming(1, { duration: 500 }),
withTiming(0.3, { duration: 500 })
),
-1,
false
);
}, []);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
return (
<Animated.View
style={[
{ width, height, borderRadius, backgroundColor: '#E5E5EA' },
animatedStyle,
]}
/>
);
};
アクティビティ インジケーター
import { ActivityIndicator } from 'react-native';
// プラットフォームネイティブ インジケーターを使用します
<ActivityIndicator size="large" color="#007AFF" />
// ローディング状態のボタン
const LoadingButton = ({ loading, title, onPress }) => (
<Pressable
onPress={onPress}
disabled={loading}
style={styles.button}
>
{loading ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.buttonText}>{title}</Text>
)}
</Pressable>
);
アクセシビリティ
VoiceOver / TalkBack
// アクセシブル ボタン
<Pressable
onPress={onPress}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Submit form"
accessibilityHint="Double tap to submit your information"
>
<Text>Submit</Text>
</Pressable>
// アクセシブル 画像
<Image
source={icon}
accessible={true}
accessibilityRole="image"
accessibilityLabel="User profile picture"
/>
// 関連要素をグループ化
<View
accessible={true}
accessibilityRole="summary"
accessibilityLabel={`${name}, ${role}, ${status}`}
>
<Text>{name}</Text>
<Text>{role}</Text>
<Text>{status}</Text>
</View>
ダイナミック タイプ (iOS)
import { PixelRatio } from 'react-native';
// システム設定に応じてフォントをスケール
const fontScale = PixelRatio.getFontScale();
const scaledFontSize = (size: number) => size * fontScale;
// または allowFontScaling を使用
<Text allowFontScaling={true} style={{ fontSize: 17 }}>
This text scales with system settings
</Text>
アンチパターン
しないこと
✗ 44pt より小さいタッチターゲット
✗ 12pt より小さいテキスト
✗ ホバー状態 (モバイルではホバーなし)
✗ 大きいテキストで壊れる固定高さ
✗ セーフエリアを無視する
✗ Android 上の重いシャドウ (elevation を使用)
✗ 背景をチェックせずに明色背景に白いテキスト
✗ ネイティブでないアニメーション (JS 駆動トランスフォーム)
✗ プラットフォーム規約を無視する (iOS vs Android)
✗ どこでもインライン スタイル (StyleSheet.create を使用)
よくある間違い
// ✗ アクセシビリティを壊すハードコーディングされたディメンション
style={{ height: 40 }} // テキストがより大きくなる可能性があります
// ✓ パディング付きの最小高さ
style={{ minHeight: 44, paddingVertical: 12 }}
// ✗ Android 上のシャドウ
shadowColor: '#000' // 動作しません
// ✓ プラットフォーム固有
...Platform.select({
ios: { shadowColor: '#000', ... },
android: { elevation: 4 },
})
// ✗ 固定ステータスバー高さ
paddingTop: 44
// ✓ セーフエリアを使用
paddingTop: insets.top
クイック リファレンス
モバイル デフォルト
タッチターゲット: 最小 44pt
フォントサイズ: 最小 12pt、本文 17pt、大きなタイトル 34pt
ボーダー半径: 10-14pt (iOS)、12-28pt (Android)
スペーシング: 4/8/16/24/32 グリッド
アニメーション: 200-300ms、ネイティブドライバー
シャドウ: iOS shadowOpacity 0.08-0.15、Android elevation 2-8
プレミアム外観チェックリスト
□ すべてのタッチターゲット 44pt 以上
□ 一貫したスペーシング (4pt グリッド)
□ プラットフォーム適切なスタイリング
□ セーフエリア処理
□ ネイティブ アニメーション (60fps)
□ 適切なローディング状態
□ ダークモード サポート
□ アクセシビリティ ラベル
□ アクション時のハプティック フィードバック
□ 適切な場所に下にスワイプして更新
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- alinaqi
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/alinaqi/claude-bootstrap / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。