Ui Mobile

by alinaqi72a456e6ccf0No license707 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 weeks ago

Mobile UI patterns - React Native, iOS/Android, touch targets

AI-generated overview

Provides mobile UI design guidance for React Native, covering touch targets, contrast, accessibility and platform patterns.

What it does
This skill supplies reference guidance and code snippets for building mobile user interfaces, mainly with React Native. It covers mandatory accessibility rules such as 44-point touch targets, WCAG contrast ratios, visible button boundaries, accessibility labels and pressed states. It also documents platform differences between iOS and Android, spacing, typography, color systems, component patterns, navigation, animations, loading states and anti-patterns.
When to use it
Use it when building or reviewing mobile UI components, especially React Native screens, buttons, lists, forms and navigation. It is also useful when checking mobile accessibility, touch target sizing or iOS versus Android styling conventions.
Requirements
No scripts or assets are included; it is instructions only. The examples assume a React Native project, with optional references to react-native-safe-area-context, react-native-reanimated and platform APIs.

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 elementsconst MINIMUM_TOUCH_SIZE = 44;
// EVERY button, link, icon button must meet thisconst 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 SMALLstyle={{ 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 valuesconst 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 scaleconst spacing = {  xs: 4,  sm: 8,  md: 16,  lg: 24,  xl: 32,  '2xl': 48,} as const;
// Usageconst 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 colorsconst 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 variantsconst 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;};
// Usageconst 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 targetsconst touchable = {  minHeight: 44,  minWidth: 44,};
// Button with proper sizingconst 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 feedbackconst Button = ({ children, onPress }) => (  <Pressable    onPress={onPress}    style={({ pressed }) => [      styles.button,      pressed && { opacity: 0.7 },    ]}  >    {children}  </Pressable>);
// Android-style rippleconst 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 buttonconst 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 buttonconst 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 sizingconst 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 itemconst 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 possibleconst fadeIn = (value: Animated.Value) => {  Animated.timing(value, {    toValue: 1,    duration: 200,    useNativeDriver: true, // CRITICAL for performance  }).start();};
// Spring for natural feelconst 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 stateconst 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 settingsconst 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 accessibilitystyle={{ height: 40 }}  // Text might be larger
// ✓ Minimum height with paddingstyle={{ minHeight: 44, paddingVertical: 12 }}
// ✗ Shadow on AndroidshadowColor: '#000'  // Won't work
// ✓ Platform-specific...Platform.select({  ios: { shadowColor: '#000', ... },  android: { elevation: 4 },})
// ✗ Fixed status bar heightpaddingTop: 44
// ✓ Use safe areapaddingTop: insets.top

Quick Reference

Mobile Defaults

Touch targets: 44pt minimumFont sizes: 12pt min, 17pt body, 34pt large titleBorder radius: 10-14pt (iOS), 12-28pt (Android)Spacing: 4/8/16/24/32 gridAnimations: 200-300ms, native driverShadow: 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 and attribution

Source:alinaqi/maggyinskills/ui-mobileat commit72a456e

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal