Web
ui-mobile - Claude MCP Skill
Mobile UI patterns - React Native, iOS/Android, touch targets
SEO Guide: Enhance your AI agent with the ui-mobile tool. This Model Context Protocol (MCP) server allows Claude Desktop and other LLMs to mobile ui patterns - react native, ios/android, touch targets... Download and configure this skill to unlock new capabilities for your AI workflow.
Documentation
SKILL.md# Mobile UI Design Skill (React Native)
---
## MANDATORY: Mobile Accessibility Standards
**These rules are NON-NEGOTIABLE. Every UI element must pass these checks.**
### 1. Touch Targets (CRITICAL)
```typescript
// MINIMUM 44x44 points for ALL interactive elements
const MINIMUM_TOUCH_SIZE = 44;
// EVERY button, link, icon button must meet this
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',
},
});
// NEVER DO THIS:
style={{ height: 30 }} // ā TOO SMALL
style={{ padding: 4 }} // ā RESULTS IN TINY TARGET
```
### 2. Color Contrast (CRITICAL)
```typescript
// WCAG 2.1 AA: 4.5:1 for text, 3:1 for large text/UI
// SAFE COMBINATIONS:
const colors = {
// Light mode
textPrimary: '#000000', // on white = 21:1 ā
textSecondary: '#374151', // gray-700 on white = 9.2:1 ā
// Dark mode
textPrimaryDark: '#FFFFFF', // on gray-900 = 16:1 ā
textSecondaryDark: '#E5E7EB', // gray-200 on gray-900 = 11:1 ā
};
// FORBIDDEN - FAILS CONTRAST:
// ā '#9CA3AF' (gray-400) on white = 2.6:1
// ā '#6B7280' (gray-500) on '#111827' = 4.0:1
// ā Any text below 4.5:1 ratio
```
### 3. Visibility Rules
```typescript
// ALL BUTTONS MUST HAVE visible boundaries
// PRIMARY: Solid background with contrasting text
<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,
},
});
// SECONDARY: Visible background
<Pressable style={styles.secondaryButton}>
<Text style={{ color: '#1F2937' }}>Cancel</Text>
</Pressable>
const styles = StyleSheet.create({
secondaryButton: {
backgroundColor: '#F3F4F6', // gray-100
minHeight: 44,
},
});
// GHOST: MUST have visible border
<Pressable style={styles.ghostButton}>
<Text style={{ color: '#374151' }}>Skip</Text>
</Pressable>
const styles = StyleSheet.create({
ghostButton: {
borderWidth: 1,
borderColor: '#D1D5DB', // gray-300
minHeight: 44,
},
});
// NEVER CREATE invisible buttons:
// ā backgroundColor: 'transparent' without border
// ā Text color matching background
```
### 4. Accessibility Labels (REQUIRED)
```tsx
// EVERY interactive element needs accessibility props
// Buttons
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="Submit form"
accessibilityHint="Double tap to submit your information"
>
<Text>Submit</Text>
</Pressable>
// Icon buttons (NO visible text = MUST have label)
<Pressable
accessible={true}
accessibilityRole="button"
accessibilityLabel="Close menu"
>
<CloseIcon />
</Pressable>
// Images
<Image
accessible={true}
accessibilityRole="image"
accessibilityLabel="User profile photo"
source={...}
/>
```
### 5. Focus/Selection States
```tsx
// EVERY Pressable needs visible pressed state
<Pressable
style={({ pressed }) => [
styles.button,
pressed && styles.buttonPressed,
]}
>
{children}
</Pressable>
const styles = StyleSheet.create({
button: {
backgroundColor: '#1F2937',
},
buttonPressed: {
opacity: 0.7,
// OR
backgroundColor: '#374151',
},
});
```
---
## Core Philosophy
**Mobile UI is about touch, speed, and focus.** No hover states, smaller screens, thumb-friendly targets. Design for one-handed use and interruption recovery.
## Platform Differences
### iOS vs Android
```typescript
import { Platform } from 'react-native';
// Platform-specific values
const styles = StyleSheet.create({
shadow: Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
android: {
elevation: 4,
},
}),
// iOS uses SF Pro, Android uses Roboto
text: {
fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
},
});
```
### Design Language
```
iOS (Human Interface Guidelines)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
- Flat design with subtle depth
- SF Symbols for icons
- Large titles (34pt)
- Rounded corners (10-14pt)
- Blue as default tint
Android (Material Design 3)
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
- Material You dynamic color
- Outlined/filled icons
- Medium titles (22pt)
- Rounded corners (12-28pt)
- Primary color from theme
```
## Spacing System
### 4px Base Grid
```typescript
// React Native spacing - consistent scale
const spacing = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
'2xl': 48,
} as const;
// Usage
const styles = StyleSheet.create({
container: {
padding: spacing.md,
gap: spacing.sm,
},
});
```
### Safe Areas
```tsx
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>
);
};
```
## Typography
### Type Scale
```typescript
const typography = {
// Large titles (iOS style)
largeTitle: {
fontSize: 34,
fontWeight: '700' as const,
letterSpacing: 0.37,
},
// Section headers
title: {
fontSize: 22,
fontWeight: '700' as const,
letterSpacing: 0.35,
},
// Card titles
headline: {
fontSize: 17,
fontWeight: '600' as const,
letterSpacing: -0.41,
},
// Body text
body: {
fontSize: 17,
fontWeight: '400' as const,
letterSpacing: -0.41,
lineHeight: 22,
},
// Secondary text
callout: {
fontSize: 16,
fontWeight: '400' as const,
letterSpacing: -0.32,
},
// Small labels
caption: {
fontSize: 12,
fontWeight: '400' as const,
letterSpacing: 0,
},
};
```
## Color System
### Semantic Colors
```typescript
// Use semantic names, not literal colors
const colors = {
// Backgrounds
background: '#FFFFFF',
backgroundSecondary: '#F2F2F7',
backgroundTertiary: '#FFFFFF',
// Surfaces
surface: '#FFFFFF',
surfaceElevated: '#FFFFFF',
// Text
label: '#000000',
labelSecondary: '#3C3C43', // 60% opacity
labelTertiary: '#3C3C43', // 30% opacity
// Actions
primary: '#007AFF',
destructive: '#FF3B30',
success: '#34C759',
warning: '#FF9500',
// Separators
separator: '#3C3C43', // 29% opacity
opaqueSeparator: '#C6C6C8',
};
// Dark mode variants
const darkColors = {
background: '#000000',
backgroundSecondary: '#1C1C1E',
label: '#FFFFFF',
labelSecondary: '#EBEBF5', // 60% opacity
separator: '#545458',
};
```
### Dynamic Colors (React Native)
```tsx
import { useColorScheme } from 'react-native';
const useColors = () => {
const scheme = useColorScheme();
return scheme === 'dark' ? darkColors : colors;
};
// Usage
const MyComponent = () => {
const colors = useColors();
return (
<View style={{ backgroundColor: colors.background }}>
<Text style={{ color: colors.label }}>Hello</Text>
</View>
);
};
```
## Touch Targets
### Minimum Sizes
```typescript
// CRITICAL: Minimum 44pt touch targets
const touchable = {
minHeight: 44,
minWidth: 44,
};
// Button with proper sizing
const styles = StyleSheet.create({
button: {
minHeight: 44,
paddingHorizontal: 16,
paddingVertical: 12,
justifyContent: 'center',
alignItems: 'center',
},
// Icon button (square)
iconButton: {
width: 44,
height: 44,
justifyContent: 'center',
alignItems: 'center',
},
// List row
listRow: {
minHeight: 44,
paddingVertical: 12,
paddingHorizontal: 16,
},
});
```
### Touch Feedback
```tsx
import { Pressable } from 'react-native';
// iOS-style opacity feedback
const Button = ({ children, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.button,
pressed && { opacity: 0.7 },
]}
>
{children}
</Pressable>
);
// Android-style ripple
const AndroidButton = ({ children, onPress }) => (
<Pressable
onPress={onPress}
android_ripple={{
color: 'rgba(0, 0, 0, 0.1)',
borderless: false,
}}
style={styles.button}
>
{children}
</Pressable>
);
```
## Component Patterns
### Cards
```tsx
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,
},
}),
},
});
```
### Buttons
```tsx
// Primary button
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,
},
});
// Secondary button
const SecondaryButton = ({ title, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [
styles.secondaryButton,
pressed && { opacity: 0.7 },
]}
>
<Text style={styles.secondaryButtonText}>{title}</Text>
</Pressable>
);
```
### Input Fields
```tsx
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',
},
});
```
### Lists
```tsx
// Grouped list (iOS Settings style)
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,
},
});
```
## Navigation Patterns
### Bottom Tab Bar
```tsx
// Proper bottom tab sizing
const tabBarStyle = {
height: Platform.OS === 'ios' ? 83 : 65, // Account for home indicator
paddingBottom: Platform.OS === 'ios' ? 34 : 10,
paddingTop: 10,
backgroundColor: '#F8F8F8',
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#C6C6C8',
};
// Tab item
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>
);
```
### Header
```tsx
// Large title header (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,
},
});
```
## Animations
### Native Driver Animations
```tsx
import { Animated } from 'react-native';
// Always use native driver when possible
const fadeIn = (value: Animated.Value) => {
Animated.timing(value, {
toValue: 1,
duration: 200,
useNativeDriver: true, // CRITICAL for performance
}).start();
};
// Spring for natural feel
const bounce = (value: Animated.Value) => {
Animated.spring(value, {
toValue: 1,
damping: 15,
stiffness: 150,
useNativeDriver: true,
}).start();
};
```
### Reanimated for Complex Animations
```tsx
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>
);
};
```
## Loading States
### Skeleton Loader
```tsx
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,
]}
/>
);
};
```
### Activity Indicator
```tsx
import { ActivityIndicator } from 'react-native';
// Use platform-native indicator
<ActivityIndicator size="large" color="#007AFF" />
// Button with loading state
const LoadingButton = ({ loading, title, onPress }) => (
<Pressable
onPress={onPress}
disabled={loading}
style={styles.button}
>
{loading ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.buttonText}>{title}</Text>
)}
</Pressable>
);
```
## Accessibility
### VoiceOver / TalkBack
```tsx
// Accessible button
<Pressable
onPress={onPress}
accessible={true}
accessibilityRole="button"
accessibilityLabel="Submit form"
accessibilityHint="Double tap to submit your information"
>
<Text>Submit</Text>
</Pressable>
// Accessible image
<Image
source={icon}
accessible={true}
accessibilityRole="image"
accessibilityLabel="User profile picture"
/>
// Group related elements
<View
accessible={true}
accessibilityRole="summary"
accessibilityLabel={`${name}, ${role}, ${status}`}
>
<Text>{name}</Text>
<Text>{role}</Text>
<Text>{status}</Text>
</View>
```
### Dynamic Type (iOS)
```tsx
import { PixelRatio } from 'react-native';
// Scale fonts with system settings
const fontScale = PixelRatio.getFontScale();
const scaledFontSize = (size: number) => size * fontScale;
// Or use allowFontScaling
<Text allowFontScaling={true} style={{ fontSize: 17 }}>
This text scales with system settings
</Text>
```
## Anti-Patterns
### Never Do
```
ā Touch targets smaller than 44pt
ā Text smaller than 12pt
ā Hover states (no hover on mobile)
ā Fixed heights that break with large text
ā Ignoring safe areas
ā Heavy shadows on Android (use elevation)
ā White text on light backgrounds without checking contrast
ā Non-native animations (JS-driven transforms)
ā Ignoring platform conventions (iOS vs Android)
ā Inline styles everywhere (use StyleSheet.create)
```
### Common Mistakes
```tsx
// ā Hardcoded dimensions that break accessibility
style={{ height: 40 }} // Text might be larger
// ā Minimum height with padding
style={{ minHeight: 44, paddingVertical: 12 }}
// ā Shadow on Android
shadowColor: '#000' // Won't work
// ā Platform-specific
...Platform.select({
ios: { shadowColor: '#000', ... },
android: { elevation: 4 },
})
// ā Fixed status bar height
paddingTop: 44
// ā Use safe area
paddingTop: insets.top
```
## Quick Reference
### Mobile Defaults
```
Touch targets: 44pt minimum
Font sizes: 12pt min, 17pt body, 34pt large title
Border radius: 10-14pt (iOS), 12-28pt (Android)
Spacing: 4/8/16/24/32 grid
Animations: 200-300ms, native driver
Shadow: iOS shadowOpacity 0.08-0.15, Android elevation 2-8
```
### Premium Feel Checklist
```
ā” All touch targets 44pt+
ā” Consistent spacing (4pt grid)
ā” Platform-appropriate styling
ā” Safe area handling
ā” Native animations (60fps)
ā” Proper loading states
ā” Dark mode support
ā” Accessibility labels
ā” Haptic feedback on actions
ā” Pull-to-refresh where appropriate
```Signals
Information
- Repository
- alinaqi/claude-bootstrap
- Author
- alinaqi
- Last Sync
- 9/5/2026
- Repo Updated
- 9/1/2026
- Created
- 1/14/2026
Reviews (0)
No reviews yet. Be the first to review this skill!
Related Skills
firecrawl-build-search
Integrate Firecrawl `/search` into product code and agent workflows. Use when an app needs discovery before extraction, when the feature starts with a query instead of a URL, or when the system should search the web and optionally hydrate result content.
firecrawl-build-onboarding
Get Firecrawl credentials and SDK setup into a project. Use when an application needs `FIRECRAWL_API_KEY`, when an agent should add Firecrawl to `.env`, when the user wants to authenticate Firecrawl for app code, or when choosing the first SDK and docs for a new Firecrawl integration. This skill includes its own browser auth flow, so it does not depend on the website onboarding skill.
firecrawl-build
Integrate Firecrawl into application code whenever a product, agent, or workflow needs web data inside the app ā web search, live search results, page scraping, structured extraction, or browser interaction. Use when building any feature that needs data from the web in code, even if the user does not mention Firecrawl explicitly and only describes wanting web data, website content, search, scraping, or interaction in an application. Trigger for Firecrawl requests, "fire girl" shorthand, and generic app-level web-data needs that should map to `/scrape`, `/search`, or `/interact`. Do not use this skill for one-off terminal-only web tasks during the current session; use `firecrawl/cli` for those.
firecrawl-build-interact
Integrate Firecrawl `/interact` into product code for dynamic pages and browser actions after scraping. Use when a feature needs clicks, form fills, pagination, authentication-aware flows, or other multi-step interactions that plain `/scrape` cannot complete.
Related Guides
Mastering Python and TypeScript Development with the Claude Skill Guide
Learn how to use the python typescript guide Claude skill. Complete guide with installation instructions and examples.
Mastering VSCode Extension Development with Claude: A Complete Guide to the TypeScript Extension Dev Skill
Learn how to use the vscode extension dev typescript Claude skill. Complete guide with installation instructions and examples.
Building Full-Stack Applications with Claude: The Node.js MongoDB JWT Express React Skill Guide
Learn how to use the nodejs mongodb jwt express react Claude skill. Complete guide with installation instructions and examples.