| name | ui-mobile |
| description | Mobile UI patterns - React Native, iOS/Android, touch targets Use when this capability is needed. |
| metadata | {"author":"EmmanuelOrtiz87"} |
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)
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. Color Contrast (CRITICAL)
const colors = {
textPrimary: '#000000',
textSecondary: '#374151',
textPrimaryDark: '#FFFFFF',
textSecondaryDark: '#E5E7EB',
};
3. Visibility Rules
<Pressable style={styles.primaryButton}>
<Text style={{ color: '#FFFFFF' }}>Submit</Text>
</Pressable>
const styles = StyleSheet.create({
primaryButton: {
backgroundColor: '#1F2937',
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',
minHeight: 44,
},
});
styles = .({
: {
: ,
: ,
: ,
},
});
4. Accessibility Labels (REQUIRED)
<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. Focus/Selection States
<Pressable style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}>
{children}
</Pressable>;
const styles = StyleSheet.create({
button: {
backgroundColor: '#1F2937',
},
buttonPressed: {
opacity: 0.7,
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
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,
},
}),
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
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,
},
});
Safe Areas
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
const typography = {
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,
},
: {
: ,
: ,
: ,
},
};
Color System
Semantic Colors
const colors = {
background: '#FFFFFF',
backgroundSecondary: '#F2F2F7',
backgroundTertiary: '#FFFFFF',
surface: '#FFFFFF',
surfaceElevated: '#FFFFFF',
label: '#000000',
labelSecondary: '#3C3C43',
labelTertiary: '#3C3C43',
primary: '#007AFF',
destructive: '#FF3B30',
success: '#34C759',
warning: '#FF9500',
separator: '#3C3C43',
opaqueSeparator: '#C6C6C8',
};
const darkColors = {
background: '#000000',
backgroundSecondary: '#1C1C1E',
label: '#FFFFFF',
labelSecondary: '#EBEBF5',
separator: '#545458',
};
Dynamic Colors (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>
);
};
Touch Targets
Minimum Sizes
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,
},
});
Touch Feedback
import { Pressable } from 'react-native';
const Button = ({ children, onPress }) => (
<Pressable
onPress={onPress}
style={({ pressed }) => [styles.button, pressed && { opacity: 0.7 }]}
>
{children}
</Pressable>
);
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
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
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 = () => (
);
Input Fields
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: ,
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
},
: {
: ,
},
: {
: ,
: ,
},
});
Lists
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>
))}
);
styles = .({
: {
: ,
: ,
},
: {
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
},
: {
: .,
: ,
: ,
},
});
Navigation Patterns
Bottom Tab Bar
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>
);
Header
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: ,
: ,
},
});
Animations
Native Driver Animations
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 for Complex Animations
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
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
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>
);
Accessibility
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>
Dynamic Type (iOS)
import { PixelRatio } from 'react-native';
const fontScale = PixelRatio.getFontScale();
const scaledFontSize = (size: number) => size * fontScale;
<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
style={{ height: 40 }}
style={{ minHeight: 44, paddingVertical: 12 }}
shadowColor: '#000'
...Platform.select({
ios: { shadowColor: '#000', ... },
android: { elevation: 4 },
})
paddingTop: 44
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
Source: EmmanuelOrtiz87/gentle-vanguard — distributed by TomeVault.