ios-design-guidelines
iOS Human Interface Guidelines reference. Use when designing mobile UI, validating touch targets, implementing gestures, or ensuring iOS compliance.
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: ios-design-guidelines
description: iOS Human Interface Guidelines reference. Use when designing mobile UI, validating touch targets, implementing gestures, or ensuring iOS compliance.
---
# iOS Design Guidelines
This skill provides iOS Human Interface Guidelines (HIG) reference for building native-feeling iOS apps.
## When to Use This Skill
- Designing new mobile UI screens or components
- Validating touch target sizes
- Implementing iOS-standard navigation patterns
- Choosing appropriate gestures
- Ensuring iOS compliance and native feel
## Touch Targets & Spacing
### Minimum Touch Target Size
**44 × 44 points minimum** for all interactive elements.
```typescript
// ✅ CORRECT: 44pt minimum
<Pressable className="w-11 h-11"> {/* 44 × 44pt */}
<Icon size={24} />
</Pressable>
<Pressable className="p-3"> {/* Padding creates 44pt+ target */}
<Icon size={20} />
</Pressable>
// ❌ WRONG: Too small
<Pressable className="w-8 h-8"> {/* Only 32pt */}
<Icon size={24} />
</Pressable>
```
### Spacing Between Touch Targets
**8 points minimum** spacing between adjacent interactive elements.
```typescript
// ✅ CORRECT: 8pt spacing
<View className="flex-row gap-2"> {/* gap-2 = 8pt */}
<Pressable className="w-11 h-11">
<Icon name="heart" />
</Pressable>
<Pressable className="w-11 h-11">
<Icon name="share" />
</Pressable>
</View>
// ❌ WRONG: Too close
<View className="flex-row gap-1"> {/* Only 4pt - too close */}
<Pressable className="w-11 h-11">...</Pressable>
<Pressable className="w-11 h-11">...</Pressable>
</View>
```
### Conversion Guide
- 44pt = `w-11 h-11` in Tailwind (44 ÷ 4 = 11)
- 8pt = `gap-2` or `mx-2` (8 ÷ 4 = 2)
- 16pt = `p-4` (16 ÷ 4 = 4)
## Safe Areas
### Safe Area Insets
iOS devices have non-rectangular screens with insets:
**Top insets**:
- Status bar: ~47pt (includes status bar + notch/Dynamic Island)
- Without notch: ~47pt (status bar only)
**Bottom insets**:
- Home indicator: 34pt (iPhone without home button)
- With home button: 0pt (older iPhones)
### Implementation
Always use `SafeAreaView` from `react-native-safe-area-context`:
```typescript
import { SafeAreaView } from 'react-native-safe-area-context'
// Screen component
export const HomeScreen = () => (
<SafeAreaView className="flex-1 bg-white">
{/* Content automatically avoids notch and home indicator */}
</SafeAreaView>
)
// Modal with selective edges
export const ModalScreen = () => (
<SafeAreaView edges={['top', 'left', 'right']} className="flex-1">
{/* Excludes bottom edge - useful for modals with bottom button */}
<ScrollView className="flex-1">
{/* Content */}
</ScrollView>
<View className="p-4 pb-8"> {/* Extra bottom padding for home indicator */}
<Button>Submit</Button>
</View>
</SafeAreaView>
)
```
## Navigation Patterns
### Tab Bar (Bottom Tabs)
- **Position**: Bottom of screen
- **Items**: 3-5 tabs (max 5 per iOS HIG)
- **Height**: 49pt (compact) or 65pt (with labels)
- **Active state**: Icon + label highlighted
```typescript
// React Navigation Bottom Tabs setup
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: '#007AFF', // iOS blue
tabBarInactiveTintColor: '#8E8E93', // iOS gray
tabBarStyle: {
height: 65,
paddingBottom: 8,
},
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Explore" component={ExploreScreen} />
<Tab.Screen name="Friends" component={FriendsScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
```
**Best practices**:
- Use clear, recognizable icons (SF Symbols style)
- Keep labels short (1-2 words)
- Don't exceed 5 tabs
- Always show icons, optionally show labels
### Navigation Bar (Top Bar)
- **Height**: 44pt (compact) or 96pt (large title)
- **Back button**: Left side with chevron (<)
- **Title**: Center or large (iOS 11+)
- **Actions**: Right side (1-2 buttons max)
```typescript
<Stack.Screen
name="PlaceDetails"
options={{
title: "Place Details",
headerBackTitle: "Back", // Short back button text
headerLargeTitle: false, // Use large title style
headerRight: () => (
<Pressable onPress={handleShare}>
<ShareIcon />
</Pressable>
),
}}
/>
```
### Modal Presentation
- **Presentation**: Card from bottom (standard) or full screen
- **Dismissal**: Swipe down or Cancel button (top-left)
- **Animation**: 440ms spring (presentation), 320ms (dismissal)
```typescript
<Stack.Screen
name="CreatePlace"
component={CreatePlaceScreen}
options={{
presentation: 'modal', // Slides up from bottom
headerLeft: () => <CancelButton />, // Cancel instead of Back
gestureEnabled: true, // Allow swipe to dismiss
}}
/>
```
### Sheet (Bottom Sheet)
- **Height**: Partial (1/3 to 2/3 screen) or full
- **Dismissal**: Swipe down or tap outside
- **Handle**: Visual grabber at top (optional)
```typescript
// Using react-native-bottom-sheet or custom implementation
<BottomSheet
snapPoints={['25%', '50%', '90%']}
enablePanDownToClose={true}
>
{/* Sheet content */}
</BottomSheet>
```
## Gestures
### Standard iOS Gestures
- **Tap**: Select, activate, or trigger action
- **Swipe**: Navigate back (left edge), delete (list item), or scroll
- **Long press**: Show context menu or drag to reorder
- **Pinch**: Zoom in/out
- **Pull to refresh**: Reload content
### Swipe Back Gesture
iOS users expect to swipe from left edge to go back:
```typescript
// React Navigation enables this by default
<Stack.Navigator
screenOptions={{
gestureEnabled: true, // Enable swipe back (default)
fullScreenGestureEnabled: false, // Only edge swipe (recommended)
}}
>
{/* Screens */}
</Stack.Navigator>
```
### Pull to Refresh
```typescript
import { RefreshControl } from 'react-native'
<ScrollView
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor="#007AFF" // iOS blue spinner
/>
}
>
{/* Content */}
</ScrollView>
// Or with FlatList
<FlatList
data={items}
renderItem={renderItem}
refreshing={isRefreshing}
onRefresh={handleRefresh}
/>
```
### Swipe to Delete (List Items)
```typescript
// Use react-native-gesture-handler for swipe actions
import { Swipeable } from 'react-native-gesture-handler'
<Swipeable
renderRightActions={() => (
<Pressable onPress={handleDelete} className="bg-red-500 justify-center px-4">
<Text className="text-white">Delete</Text>
</Pressable>
)}
>
<ListItem item={item} />
</Swipeable>
```
## Typography
### SF Pro Font Family
iOS uses SF Pro (system font). In React Native, use default system font:
```typescript
// ✅ CORRECT: Let iOS use system font
<Text className="text-base font-medium">
Hello World
</Text>
// Font weights
<Text className="font-normal">Regular (400)</Text>
<Text className="font-medium">Medium (500)</Text>
<Text className="font-semibold">Semibold (600)</Text>
<Text className="font-bold">Bold (700)</Text>
```
### Type Scale
| Use Case | Size | Line Height | Weight |
|----------|------|-------------|--------|
| Large Title | 34pt | 41pt | Bold |
| Title 1 | 28pt | 34pt | Bold |
| Title 2 | 22pt | 28pt | Bold |
| Title 3 | 20pt | 25pt | Semibold |
| Headline | 17pt | 22pt | Semibold |
| Body | 17pt | 22pt | Regular |
| Callout | 16pt | 21pt | Regular |
| Subheadline | 15pt | 20pt | Regular |
| Footnote | 13pt | 18pt | Regular |
| Caption 1 | 12pt | 16pt | Regular |
| Caption 2 | 11pt | 13pt | Regular |
```typescript
// Implementation examples
<Text className="text-[34px] leading-[41px] font-bold">Large Title</Text>
<Text className="text-[17px] leading-[22px]">Body</Text>
<Text className="text-[13px] leading-[18px] text-gray-600">Footnote</Text>
```
### Line Height Recommendations
- **Headings**: 1.2-1.3× font size
- **Body text**: 1.4-1.5× font size
- **Tight spacing**: 1.2× (for UI labels)
## Colors
### System Colors
iOS provides semantic colors that adapt to light/dark mode:
```typescript
// Use Tailwind colors that approximate iOS system colors
const colors = {
// Primary
blue: '#007AFF', // System blue (links, primary actions)
green: '#34C759', // System green (success)
red: '#FF3B30', // System red (destructive)
orange: '#FF9500', // System orange (warning)
// Grays (light mode)
gray: '#8E8E93', // Secondary text
gray2: '#AEAEB2', // Tertiary text
gray3: '#C7C7CC', // Borders
gray4: '#D1D1D6', // Separators
gray5: '#E5E5EA', // Backgrounds
gray6: '#F2F2F7', // Secondary backgrounds
// Text
label: '#000000', // Primary text (black in light mode)
secondaryLabel: '#3C3C43', // Secondary text (60% opacity)
tertiaryLabel: '#3C3C4399', // Tertiary text (30% opacity)
// Backgrounds
background: '#FFFFFF', // Primary background
secondaryBackground: '#F2F2F7', // Secondary background
tertiaryBackground: '#FFFFFF', // Tertiary background
}
```
### Usage
```typescript
// Primary action (blue)
<Pressable className="bg-[#007AFF]">
<Text className="text-white">Save</Text>
</Pressable>
// Destructive action (red)
<Pressable className="bg-[#FF3B30]">
<Text className="text-white">Delete</Text>
</Pressable>
// Secondary text
<Text className="text-[#8E8E93]">Last updated 2h ago</Text>
```
## Spacing & Layout
### 8-Point Grid System
All spacing should be multiples of 8pt:
```typescript
// ✅ CORRECT: 8pt multiples
className="p-4" // 16pt
className="p-6" // 24pt
className="mx-8" // 32pt
className="gap-2" // 8pt
// ❌ AVOID: Non-8pt values
className="p-3" // 12pt (not 8pt multiple)
className="mx-5" // 20pt (not 8pt multiple)
```
### Common Spacing Values
- **xs**: 4pt (`p-1`) - Tight spacing
- **sm**: 8pt (`p-2`) - Minimum touch target spacing
- **md**: 16pt (`p-4`) - Default padding
- **lg**: 24pt (`p-6`) - Section spacing
- **xl**: 32pt (`p-8`) - Large section spacing
### Screen Padding
```typescript
// Standard screen padding: 16pt (p-4)
<SafeAreaView className="flex-1 bg-white">
<ScrollView className="p-4">
{/* Content with 16pt padding */}
</ScrollView>
</SafeAreaView>
```
## Visual Design
### Shadows & Depth
iOS uses subtle shadows to create depth:
```typescript
const styles = StyleSheet.create({
card: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
},
floating: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 12,
},
})
```
**Guidelines**:
- Keep shadows subtle (opacity 0.05-0.15)
- Use vertical offset (y: 2-4)
- Larger shadowRadius for higher elevation
### Border Radius
```typescript
// iOS corner radius values
className="rounded-lg" // 8pt - Standard
className="rounded-xl" // 12pt - Cards
className="rounded-2xl" // 16pt - Large cards
className="rounded-3xl" // 24pt - Sheets
className="rounded-full" // Circle/pill
```
### Borders & Separators
```typescript
// Hairline separator (1px)
<View className="h-[1px] bg-gray-200" />
// Standard border
<View className="border border-gray-300 rounded-lg">
{/* Content */}
</View>
```
## Loading States
### Activity Indicator
```typescript
import { ActivityIndicator } from 'react-native'
<ActivityIndicator size="large" color="#007AFF" />
// In loading state
{isLoading && (
<View className="flex-1 items-center justify-center">
<ActivityIndicator size="large" />
</View>
)}
```
### Skeleton Loading
More iOS-native than spinners for content:
```typescript
// Animated skeleton
<View className="bg-gray-200 h-4 rounded animate-pulse" />
```
## iOS-Specific Patterns
### Search Bar
```typescript
// iOS-style search
<View className="px-4 py-2">
<View className="flex-row items-center bg-gray-100 rounded-lg px-3 py-2">
<SearchIcon className="text-gray-400 mr-2" />
<TextInput
placeholder="Search"
placeholderTextColor="#8E8E93"
className="flex-1 text-base"
/>
</View>
</View>
```
### Segmented Control
```typescript
// iOS segmented control style
<View className="flex-row bg-gray-100 rounded-lg p-1">
<Pressable className={`flex-1 py-2 rounded-md ${active === 'all' ? 'bg-white' : ''}`}>
<Text className={`text-center ${active === 'all' ? 'font-semibold' : ''}`}>
All
</Text>
</Pressable>
<Pressable className={`flex-1 py-2 rounded-md ${active === 'favorites' ? 'bg-white' : ''}`}>
<Text className={`text-center ${active === 'favorites' ? 'font-semibold' : ''}`}>
Favorites
</Text>
</Pressable>
</View>
```
### Context Menu (Long Press)
```typescript
// iOS-style context menu
import * as ContextMenu from 'zeego/context-menu'
<ContextMenu.Root>
<ContextMenu.Trigger>
<PlaceCard place={place} />
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item key="share" onSelect={handleShare}>
<ContextMenu.ItemTitle>Share</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon ios={{ name: 'square.and.arrow.up' }} />
</ContextMenu.Item>
<ContextMenu.Item key="delete" destructive onSelect={handleDelete}>
<ContextMenu.ItemTitle>Delete</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon ios={{ name: 'trash' }} />
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root>
```
## Accessibility
See @mobile-accessibility skill for detailed VoiceOver and accessibility patterns.
**Quick checklist**:
- 44×44pt minimum touch targets ✓
- VoiceOver labels on all interactive elements ✓
- 4.5:1 contrast ratio for text ✓
- Support Dynamic Type ✓
## Related Skills
- @mobile-accessibility - VoiceOver, Dynamic Type, WCAG compliance
- @react-native-patterns - Component implementation
- @react-navigation-patterns - Navigation implementation
---
Follow iOS HIG to create native-feeling experiences that iOS users expect. When in doubt, check how native iOS apps handle similar patterns.