Claude Agent Skill · by Alinaqi

Ui Mobile

Install Ui Mobile skill for Claude Code from alinaqi/claude-bootstrap.

Install
Terminal · npx
$npx skills add https://github.com/vercel-labs/agent-skills --skill vercel-react-best-practices
Works with Paperclip

How Ui Mobile fits into a Paperclip company.

Ui Mobile drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md954 lines
Expand
---name: ui-mobiledescription: Mobile UI patterns - React Native, iOS/Android, touch targetswhen-to-use: When building mobile UI componentsuser-invocable: falsepaths: ["**/*.tsx", "**/*.jsx", "ios/**", "android/**", "**/*.dart"]effort: medium--- # 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```typescriptimport { 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```tsximport { 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```typescriptconst 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)```tsximport { 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```tsximport { 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```tsxconst 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```tsxconst 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```tsximport { 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```tsximport 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```tsxconst 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```tsximport { 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)```tsximport { 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```