Mobile Design Skill
When to Use This Skill
Use this skill when working on:
-
Mobile-First Web Applications: Building responsive websites that prioritize mobile user experience
-
Native Mobile Apps: Designing iOS or Android applications with platform-specific patterns
-
Progressive Web Apps (PWAs): Creating app-like experiences in the browser
-
Hybrid Mobile Applications: Developing cross-platform apps using React Native, Flutter, or similar frameworks
-
Responsive Design Systems: Creating components that adapt seamlessly across devices
-
Touch-First Interfaces: Designing for touchscreen interactions rather than mouse/keyboard
-
Mobile E-commerce: Building shopping experiences optimized for small screens
-
Mobile Dashboards: Adapting data-heavy interfaces for mobile consumption
-
Gesture-Based Interfaces: Implementing swipe, pinch, and other touch gestures
-
Accessibility Audits: Ensuring mobile interfaces meet accessibility standards
This skill helps you create mobile experiences that feel native, perform well, and delight users on smartphones and tablets.
Core Concepts
Mobile-First Design Philosophy
Mobile-first design starts with the smallest screen and progressively enhances for larger devices:
Why Mobile-First?
-
Forces prioritization of essential content and features
-
Improves performance by default (lighter assets, simpler layouts)
-
Easier to scale up than scale down
-
Reflects actual user behavior (mobile traffic often exceeds desktop)
-
Ensures core functionality works on all devices
Mobile-First vs Desktop-First:
/* Mobile-First Approach (Recommended) / / Base styles for mobile */ .container { padding: 16px; font-size: 14px; }
/* Tablet enhancements */ @media (min-width: 768px) { .container { padding: 24px; font-size: 16px; } }
/* Desktop enhancements */ @media (min-width: 1024px) { .container { padding: 32px; max-width: 1200px; margin: 0 auto; } }
/* Desktop-First Approach (Not Recommended) / / Base styles for desktop */ .container { padding: 32px; max-width: 1200px; margin: 0 auto; font-size: 16px; }
/* Tablet overrides */ @media (max-width: 1023px) { .container { padding: 24px; } }
/* Mobile overrides */ @media (max-width: 767px) { .container { padding: 16px; font-size: 14px; } }
Touch Targets and Ergonomics
Minimum Touch Target Sizes:
-
Apple: 44×44 points (iOS Human Interface Guidelines)
-
Google: 48×48 dp (Material Design)
-
Microsoft: 40×40 pixels (Windows Phone)
-
Recommended: 48×48 pixels minimum, 56×56 pixels optimal
Touch Target Spacing:
-
Minimum 8px spacing between interactive elements
-
Optimal 12-16px spacing for frequently used controls
-
Edge-to-edge buttons can touch if they're different types (e.g., cancel vs confirm)
Thumb Zones:
Mobile screens have three ergonomic zones:
-
Easy Zone (Green): Bottom third, center - easiest to reach with thumb
-
Stretch Zone (Yellow): Middle area - requires slight reach
-
Difficult Zone (Red): Top corners - hardest to reach one-handed
Design Implications:
-
Place primary actions in the easy zone (bottom center)
-
Put destructive actions in difficult zones (top corners)
-
Navigation typically at top or bottom, never middle
-
Consider both left-handed and right-handed users
// React Native: Bottom-aligned primary action (easy zone) <View style={styles.container}> <ScrollView style={styles.content}> {/* Main content */} </ScrollView>
<View style={styles.bottomActions}> <TouchableOpacity style={styles.primaryButton}> <Text>Continue</Text> </TouchableOpacity> </View> </View>
const styles = StyleSheet.create({ container: { flex: 1, }, content: { flex: 1, }, bottomActions: { padding: 16, paddingBottom: 32, // Extra padding for iPhone home indicator backgroundColor: '#fff', borderTopWidth: 1, borderTopColor: '#e0e0e0', }, primaryButton: { height: 56, // Optimal touch target borderRadius: 28, backgroundColor: '#007AFF', justifyContent: 'center', alignItems: 'center', }, });
Viewport and Screen Considerations
Common Mobile Breakpoints:
/* Extra small devices (phones, 320px - 479px) */ @media (min-width: 320px) { }
/* Small devices (large phones, 480px - 767px) */ @media (min-width: 480px) { }
/* Medium devices (tablets, 768px - 1023px) */ @media (min-width: 768px) { }
/* Large devices (small laptops, 1024px - 1279px) */ @media (min-width: 1024px) { }
/* Extra large devices (desktops, 1280px and up) */ @media (min-width: 1280px) { }
Viewport Meta Tag:
<!-- Responsive viewport (required for mobile) --> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes">
<!-- PWA with standalone mode --> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
Safe Areas (iPhone X and later):
/* Account for notch and home indicator */ .header { padding-top: env(safe-area-inset-top); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
.footer { padding-bottom: env(safe-area-inset-bottom); padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-right); }
Touch Interactions
Tap (Primary Interaction)
Single Tap:
-
Primary action on buttons, links, list items
-
Should provide immediate visual feedback (0-100ms delay)
-
Minimum size: 48×48 pixels
// React: Tap with visual feedback import { useState } from 'react';
function TapButton({ onPress, children }) { const [isPressed, setIsPressed] = useState(false);
return (
<button
className={tap-button ${isPressed ? 'pressed' : ''}}
onTouchStart={() => setIsPressed(true)}
onTouchEnd={() => setIsPressed(false)}
onTouchCancel={() => setIsPressed(false)}
onClick={onPress}
>
{children}
</button>
);
}
// CSS .tap-button { padding: 16px 24px; background: #007AFF; color: white; border: none; border-radius: 8px; font-size: 16px; min-height: 48px; transition: transform 0.1s, background 0.1s; -webkit-tap-highlight-color: transparent; }
.tap-button.pressed { transform: scale(0.96); background: #0051D5; }
.tap-button:active { transform: scale(0.96); }
Double Tap:
-
Zoom in/out (maps, images)
-
Like/favorite (Instagram, Twitter)
-
Less common, use sparingly
iOS Double-Tap Zoom Prevention:
/* Prevent double-tap zoom while allowing pinch zoom */ touch-action: manipulation;
Swipe Gestures
Horizontal Swipe:
-
Navigate between screens/pages
-
Reveal actions (swipe-to-delete, swipe-to-archive)
-
Dismiss cards/modals
-
Switch tabs
// React: Swipeable list item import { useState } from 'react';
function SwipeableListItem({ children, onDelete, onArchive }) { const [touchStart, setTouchStart] = useState(null); const [touchEnd, setTouchEnd] = useState(null); const [translateX, setTranslateX] = useState(0);
const minSwipeDistance = 50;
const onTouchStart = (e) => { setTouchEnd(null); setTouchStart(e.targetTouches[0].clientX); };
const onTouchMove = (e) => { setTouchEnd(e.targetTouches[0].clientX); const distance = touchStart - e.targetTouches[0].clientX; setTranslateX(-distance); };
const onTouchEnd = () => { if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > minSwipeDistance;
const isRightSwipe = distance < -minSwipeDistance;
if (isLeftSwipe) {
setTranslateX(-80); // Show actions
} else if (isRightSwipe) {
setTranslateX(0); // Reset
} else {
setTranslateX(0); // Snap back
}
};
return ( <div className="swipeable-item-container"> <div className="swipe-actions"> <button onClick={onArchive} className="archive-btn">Archive</button> <button onClick={onDelete} className="delete-btn">Delete</button> </div>
<div
className="swipeable-item"
style={{ transform: `translateX(${translateX}px)` }}
onTouchStart={onTouchStart}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
>
{children}
</div>
</div>
); }
Vertical Swipe:
-
Pull to refresh (downward swipe from top)
-
Scroll content
-
Dismiss bottom sheets/modals (downward swipe)
// Pull to Refresh function PullToRefresh({ onRefresh, children }) { const [pulling, setPulling] = useState(false); const [pullDistance, setPullDistance] = useState(0); const threshold = 80;
const handleTouchStart = (e) => { if (window.scrollY === 0) { setPulling(true); } };
const handleTouchMove = (e) => { if (pulling && window.scrollY === 0) { const distance = e.touches[0].clientY - e.touches[0].target.getBoundingClientRect().top; setPullDistance(Math.min(distance, threshold * 1.5)); } };
const handleTouchEnd = () => { if (pullDistance >= threshold) { onRefresh(); } setPulling(false); setPullDistance(0); };
return ( <div onTouchStart={handleTouchStart} onTouchMove={handleTouchMove} onTouchEnd={handleTouchEnd} > {pullDistance > 0 && ( <div className="pull-indicator" style={{ height: pullDistance }}> {pullDistance >= threshold ? '↻ Release to refresh' : '↓ Pull to refresh'} </div> )} {children} </div> ); }
Pinch and Spread (Zoom)
Used for:
-
Image galleries
-
Maps
-
PDF viewers
-
Any zoomable content
// React: Pinch to Zoom function PinchZoomImage({ src, alt }) { const [scale, setScale] = useState(1); const [lastScale, setLastScale] = useState(1);
const handleTouchMove = (e) => { if (e.touches.length === 2) { e.preventDefault();
const touch1 = e.touches[0];
const touch2 = e.touches[1];
const distance = Math.hypot(
touch1.clientX - touch2.clientX,
touch1.clientY - touch2.clientY
);
if (lastDistance) {
const newScale = lastScale * (distance / lastDistance);
setScale(Math.max(1, Math.min(newScale, 4))); // Limit 1x to 4x
}
lastDistance = distance;
}
};
const handleTouchEnd = () => { setLastScale(scale); lastDistance = null; };
let lastDistance = null;
return (
<div
className="pinch-zoom-container"
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<img
src={src}
alt={alt}
style={{
transform: scale(${scale}),
transition: lastDistance ? 'none' : 'transform 0.2s',
}}
/>
</div>
);
}
Long Press
Used for:
-
Context menus
-
Item selection mode
-
Drag-and-drop initiation
-
Additional options
// React: Long Press Handler function useLongPress(callback, ms = 500) { const [startLongPress, setStartLongPress] = useState(false);
useEffect(() => { let timerId; if (startLongPress) { timerId = setTimeout(callback, ms); } else { clearTimeout(timerId); }
return () => {
clearTimeout(timerId);
};
}, [startLongPress, callback, ms]);
return { onTouchStart: () => setStartLongPress(true), onTouchEnd: () => setStartLongPress(false), onTouchMove: () => setStartLongPress(false), }; }
// Usage function LongPressItem({ item }) { const longPressProps = useLongPress(() => { console.log('Long press detected!'); // Show context menu }, 500);
return ( <div {...longPressProps} className="long-press-item"> {item.name} </div> ); }
Drag and Drop
// React Native: Drag and Drop import { PanResponder, Animated } from 'react-native';
function DraggableCard({ children }) { const pan = useRef(new Animated.ValueXY()).current;
const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, onPanResponderGrant: () => { pan.setOffset({ x: pan.x._value, y: pan.y._value, }); }, onPanResponderMove: Animated.event( [null, { dx: pan.x, dy: pan.y }], { useNativeDriver: false } ), onPanResponderRelease: () => { pan.flattenOffset(); Animated.spring(pan, { toValue: { x: 0, y: 0 }, useNativeDriver: true, }).start(); }, }) ).current;
return ( <Animated.View {...panResponder.panHandlers} style={{ transform: [{ translateX: pan.x }, { translateY: pan.y }], }} > {children} </Animated.View> ); }
Navigation Patterns
Tab Bar Navigation
Bottom Tab Bar (iOS standard, Android common):
// React Native: Bottom Tab Navigation import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import Icon from 'react-native-vector-icons/Ionicons';
const Tab = createBottomTabNavigator();
function AppNavigator() { return ( <Tab.Navigator screenOptions={({ route }) => ({ tabBarIcon: ({ focused, color, size }) => { let iconName;
if (route.name === 'Home') {
iconName = focused ? 'home' : 'home-outline';
} else if (route.name === 'Search') {
iconName = focused ? 'search' : 'search-outline';
} else if (route.name === 'Profile') {
iconName = focused ? 'person' : 'person-outline';
}
return <Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#007AFF',
tabBarInactiveTintColor: '#8E8E93',
tabBarStyle: {
height: 88, // Account for safe area
paddingBottom: 34, // iPhone home indicator
},
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
); }
Best Practices:
-
3-5 tabs maximum
-
Always show labels (don't rely on icons alone)
-
Highlight active tab clearly
-
Keep tabs visible at all times
-
Most important section on the left (for LTR languages)
Hamburger Menu (Drawer Navigation)
// React Native: Drawer Navigation import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function DrawerNavigator() { return ( <Drawer.Navigator screenOptions={{ drawerPosition: 'left', drawerType: 'slide', drawerStyle: { width: 280, }, headerShown: true, }} > <Drawer.Screen name="Home" component={HomeScreen} options={{ drawerIcon: ({ color, size }) => ( <Icon name="home-outline" color={color} size={size} /> ), }} /> <Drawer.Screen name="Settings" component={SettingsScreen} options={{ drawerIcon: ({ color, size }) => ( <Icon name="settings-outline" color={color} size={size} /> ), }} /> </Drawer.Navigator> ); }
When to Use:
-
Secondary navigation
-
Many navigation options (6+)
-
Infrequently accessed features
-
Settings and account options
Avoid When:
-
Primary navigation is needed
-
User needs quick access to all sections
-
You have 5 or fewer main sections (use tabs instead)
Bottom Sheets and Modals
Bottom Sheet (Material Design):
// React: Bottom Sheet function BottomSheet({ isOpen, onClose, children }) { const [startY, setStartY] = useState(0); const [currentY, setCurrentY] = useState(0);
const handleTouchStart = (e) => { setStartY(e.touches[0].clientY); };
const handleTouchMove = (e) => { const delta = e.touches[0].clientY - startY; if (delta > 0) { // Only allow downward drag setCurrentY(delta); } };
const handleTouchEnd = () => { if (currentY > 100) { // Threshold for closing onClose(); } setCurrentY(0); };
if (!isOpen) return null;
return (
<>
<div className="bottom-sheet-backdrop" onClick={onClose} />
<div
className="bottom-sheet"
style={{ transform: translateY(${currentY}px) }}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<div className="bottom-sheet-handle" />
<div className="bottom-sheet-content">
{children}
</div>
</div>
</>
);
}
// CSS .bottom-sheet-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 999; }
.bottom-sheet { position: fixed; bottom: 0; left: 0; right: 0; background: white; border-radius: 20px 20px 0 0; padding: 16px; max-height: 80vh; z-index: 1000; transition: transform 0.3s; }
.bottom-sheet-handle { width: 40px; height: 4px; background: #D1D1D6; border-radius: 2px; margin: 8px auto 16px; }
Full-Screen Modal:
// iOS-style modal with slide-up animation function Modal({ isOpen, onClose, children }) { if (!isOpen) return null;
return ( <div className="modal-overlay"> <div className="modal-container"> <div className="modal-header"> <button onClick={onClose} className="modal-close"> Done </button> </div> <div className="modal-content"> {children} </div> </div> </div> ); }
// CSS .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 1000; animation: fadeIn 0.3s; }
.modal-container { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: white; animation: slideUp 0.3s; }
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
Stack Navigation
// React Navigation: Stack Navigator import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
function StackNavigator() { return ( <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#007AFF', }, headerTintColor: '#fff', headerTitleStyle: { fontWeight: 'bold', }, cardStyleInterpolator: ({ current, layouts }) => { return { cardStyle: { transform: [ { translateX: current.progress.interpolate({ inputRange: [0, 1], outputRange: [layouts.screen.width, 0], }), }, ], }, }; }, }} > <Stack.Screen name="List" component={ListScreen} /> <Stack.Screen name="Details" component={DetailsScreen} /> <Stack.Screen name="Edit" component={EditScreen} /> </Stack.Navigator> ); }
Mobile UI Components
Cards
Material Design Card:
function Card({ image, title, subtitle, description, actions }) { return ( <div className="card"> {image && ( <div className="card-media"> <img src={image} alt={title} /> </div> )}
<div className="card-content">
<h3 className="card-title">{title}</h3>
{subtitle && <p className="card-subtitle">{subtitle}</p>}
<p className="card-description">{description}</p>
</div>
{actions && (
<div className="card-actions">
{actions}
</div>
)}
</div>
); }
// CSS .card { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); margin-bottom: 16px; }
.card-media img { width: 100%; height: 200px; object-fit: cover; }
.card-content { padding: 16px; }
.card-title { font-size: 20px; font-weight: 600; margin: 0 0 8px 0; }
.card-subtitle { font-size: 14px; color: #666; margin: 0 0 8px 0; }
.card-description { font-size: 14px; line-height: 1.5; color: #333; }
.card-actions { padding: 8px 16px 16px; display: flex; gap: 8px; }
Lists
iOS-Style List:
function IOSList({ items, onItemPress }) { return ( <div className="ios-list"> {items.map((item, index) => ( <div key={item.id} className="ios-list-item" onClick={() => onItemPress(item)} > {item.icon && ( <div className="ios-list-icon">{item.icon}</div> )}
<div className="ios-list-content">
<div className="ios-list-title">{item.title}</div>
{item.subtitle && (
<div className="ios-list-subtitle">{item.subtitle}</div>
)}
</div>
{item.badge && (
<div className="ios-list-badge">{item.badge}</div>
)}
<div className="ios-list-chevron">›</div>
</div>
))}
</div>
); }
// CSS .ios-list { background: white; border-radius: 12px; overflow: hidden; }
.ios-list-item { display: flex; align-items: center; padding: 12px 16px; min-height: 56px; border-bottom: 0.5px solid #E5E5EA; -webkit-tap-highlight-color: transparent; }
.ios-list-item:active { background: #F2F2F7; }
.ios-list-item:last-child { border-bottom: none; }
.ios-list-icon { width: 32px; height: 32px; margin-right: 12px; display: flex; align-items: center; justify-content: center; }
.ios-list-content { flex: 1; }
.ios-list-title { font-size: 17px; color: #000; }
.ios-list-subtitle { font-size: 15px; color: #8E8E93; margin-top: 2px; }
.ios-list-badge { background: #FF3B30; color: white; font-size: 13px; font-weight: 600; padding: 2px 8px; border-radius: 12px; margin-right: 8px; }
.ios-list-chevron { font-size: 24px; color: #C7C7CC; }
Forms
Mobile-Optimized Form:
function MobileForm() { return ( <form className="mobile-form"> <div className="form-group"> <label htmlFor="email">Email</label> <input id="email" type="email" inputMode="email" autoComplete="email" placeholder="you@example.com" /> </div>
<div className="form-group">
<label htmlFor="phone">Phone</label>
<input
id="phone"
type="tel"
inputMode="tel"
autoComplete="tel"
placeholder="(555) 123-4567"
/>
</div>
<div className="form-group">
<label htmlFor="amount">Amount</label>
<input
id="amount"
type="number"
inputMode="decimal"
placeholder="0.00"
/>
</div>
<button type="submit" className="submit-button">
Submit
</button>
</form>
); }
// CSS .mobile-form { padding: 16px; }
.form-group { margin-bottom: 24px; }
.form-group label { display: block; font-size: 14px; font-weight: 600; margin-bottom: 8px; color: #333; }
.form-group input { width: 100%; height: 56px; padding: 16px; font-size: 16px; /* Prevents zoom on iOS */ border: 2px solid #E5E5EA; border-radius: 12px; background: white; -webkit-appearance: none; }
.form-group input:focus { outline: none; border-color: #007AFF; }
.submit-button { width: 100%; height: 56px; background: #007AFF; color: white; border: none; border-radius: 12px; font-size: 17px; font-weight: 600; }
Input Types for Mobile Keyboards:
<!-- Email keyboard --> <input type="email" inputmode="email">
<!-- Numeric keyboard --> <input type="number" inputmode="numeric">
<!-- Decimal keyboard (includes . and ,) --> <input type="number" inputmode="decimal">
<!-- Telephone keyboard --> <input type="tel" inputmode="tel">
<!-- URL keyboard (includes .com, /, etc.) --> <input type="url" inputmode="url">
<!-- Search keyboard (includes search button) --> <input type="search" inputmode="search">
Action Sheets
// iOS-style Action Sheet function ActionSheet({ isOpen, onClose, title, options }) { if (!isOpen) return null;
return ( <> <div className="action-sheet-backdrop" onClick={onClose} /> <div className="action-sheet"> {title && <div className="action-sheet-title">{title}</div>}
<div className="action-sheet-options">
{options.map((option, index) => (
<button
key={index}
className={`action-sheet-option ${option.destructive ? 'destructive' : ''}`}
onClick={() => {
option.onPress();
onClose();
}}
>
{option.icon && <span className="option-icon">{option.icon}</span>}
{option.label}
</button>
))}
</div>
<button className="action-sheet-cancel" onClick={onClose}>
Cancel
</button>
</div>
</>
); }
// CSS .action-sheet { position: fixed; bottom: 0; left: 0; right: 0; background: transparent; z-index: 1001; padding: 8px; animation: slideUp 0.3s; }
.action-sheet-title { background: rgba(255, 255, 255, 0.95); padding: 16px; text-align: center; border-radius: 14px 14px 0 0; font-size: 13px; color: #8E8E93; }
.action-sheet-options { background: rgba(255, 255, 255, 0.95); border-radius: 14px; overflow: hidden; margin-bottom: 8px; }
.action-sheet-option { width: 100%; padding: 16px; background: transparent; border: none; border-bottom: 0.5px solid #E5E5EA; font-size: 20px; color: #007AFF; -webkit-tap-highlight-color: transparent; }
.action-sheet-option:active { background: rgba(0, 0, 0, 0.05); }
.action-sheet-option.destructive { color: #FF3B30; }
.action-sheet-cancel { width: 100%; padding: 16px; background: rgba(255, 255, 255, 0.95); border: none; border-radius: 14px; font-size: 20px; font-weight: 600; color: #007AFF; }
Platform Conventions
iOS Human Interface Guidelines
Navigation Bar:
-
Height: 44pt (plus status bar)
-
Large title: 52pt collapsible header
-
Back button always shows previous screen title
-
Right-aligned action buttons
Tab Bar:
-
Height: 49pt (plus safe area)
-
5 tabs maximum
-
Badge notifications on tab icons
-
Selected tab uses accent color
Typography:
-
SF Pro (system font)
-
Dynamic Type support required
-
Font sizes: 11pt to 34pt
-
Weight hierarchy: Regular, Medium, Semibold, Bold
Colors:
-
System colors adapt to light/dark mode
-
Blue (#007AFF) for tappable elements
-
Red (#FF3B30) for destructive actions
-
Semantic colors: label, secondaryLabel, tertiaryLabel
Spacing:
-
Minimum margins: 16pt
-
Standard spacing: 8pt, 16pt, 24pt, 32pt
-
Component padding: 16pt horizontal, 12pt vertical
// SwiftUI: iOS Navigation struct ContentView: View { var body: some View { NavigationView { List(items) { item in NavigationLink(destination: DetailView(item: item)) { HStack { Image(systemName: item.icon) .foregroundColor(.accentColor)
VStack(alignment: .leading) {
Text(item.title)
.font(.headline)
Text(item.subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
}
.navigationTitle("Items")
.navigationBarTitleDisplayMode(.large)
}
}
}
Material Design (Android)
App Bar:
-
Height: 56dp (64dp for tablets)
-
Elevation: 4dp
-
Hamburger icon or back arrow on left
-
Title centered or left-aligned
-
Action icons on right (max 3)
Bottom Navigation:
-
Height: 56dp
-
3-5 destinations
-
Icons with text labels
-
Active indicator
FAB (Floating Action Button):
-
Size: 56×56dp (regular), 40×40dp (mini)
-
Position: 16dp from edges
-
Primary action only
-
Extended FAB includes text label
Typography:
-
Roboto font family
-
Scale: 12sp to 96sp
-
Line height: 1.5× font size
-
Letter spacing varies by size
Elevation:
-
Shadow depth indicates hierarchy
-
0dp: flat surface
-
1-8dp: raised components
-
16-24dp: modals and dialogs
Spacing:
-
4dp grid system
-
Keylines: 16dp, 72dp from edges
-
Component spacing: 8dp, 16dp, 24dp
// Jetpack Compose: Material Design @Composable fun MaterialCard(item: Item) { Card( modifier = Modifier .fillMaxWidth() .padding(16.dp), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) ) { Column(modifier = Modifier.padding(16.dp)) { Text( text = item.title, style = MaterialTheme.typography.headlineSmall )
Spacer(modifier = Modifier.height(8.dp))
Text(
text = item.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { /* Action */ }) {
Text("ACTION")
}
}
}
}
}
Accessibility
Touch Target Sizes
WCAG 2.1 Level AAA:
-
Minimum: 44×44 pixels
-
Recommended: 48×48 pixels or larger
-
Spacing: 8px minimum between targets
// Accessible button component
function AccessibleButton({ children, onPress, variant = 'primary' }) {
return (
<button
className={accessible-button ${variant}}
onClick={onPress}
style={{
minWidth: '48px',
minHeight: '48px',
padding: '12px 24px',
}}
>
{children}
</button>
);
}
Screen Reader Support
Semantic HTML:
function AccessibleMobileNav() { return ( <nav role="navigation" aria-label="Main navigation"> <ul> <li> <a href="/home" aria-current="page"> <Icon name="home" aria-hidden="true" /> <span>Home</span> </a> </li> <li> <a href="/search"> <Icon name="search" aria-hidden="true" /> <span>Search</span> </a> </li> </ul> </nav> ); }
React Native Accessibility:
import { View, Text, TouchableOpacity } from 'react-native';
function AccessibleCard({ title, description, onPress }) {
return (
<TouchableOpacity
accessible={true}
accessibilityLabel={${title}. ${description}}
accessibilityRole="button"
accessibilityHint="Double tap to view details"
onPress={onPress}
>
<View>
<Text>{title}</Text>
<Text>{description}</Text>
</View>
</TouchableOpacity>
);
}
Color Contrast
WCAG AA Requirements:
-
Normal text: 4.5:1 contrast ratio
-
Large text (18pt+): 3:1 contrast ratio
-
UI components: 3:1 contrast ratio
/* Good contrast examples / .primary-button { background: #0066CC; / Blue / color: #FFFFFF; / White - 6.4:1 ratio */ }
.secondary-button { background: #FFFFFF; /* White / color: #333333; / Dark gray - 12.6:1 ratio */ border: 2px solid #333333; }
/* Bad contrast (avoid) / .bad-button { background: #FFCC00; / Yellow / color: #FFFFFF; / White - 1.4:1 ratio ❌ */ }
Focus Indicators
/* Visible focus states for keyboard navigation */ button:focus-visible { outline: 3px solid #007AFF; outline-offset: 2px; }
input:focus-visible { border-color: #007AFF; box-shadow: 0 0 0 4px rgba(0, 122, 255, 0.2); }
/* Remove default focus ring, add custom */ *:focus { outline: none; }
*:focus-visible { outline: 3px solid #007AFF; outline-offset: 2px; }
Performance
Image Optimization
Responsive Images:
<!-- Serve different sizes based on screen width --> <img src="image-800w.jpg" srcset=" image-400w.jpg 400w, image-800w.jpg 800w, image-1200w.jpg 1200w " sizes=" (max-width: 480px) 100vw, (max-width: 768px) 50vw, 33vw " alt="Product image" loading="lazy"
<!-- WebP with fallback --> <picture> <source srcset="image.webp" type="image/webp"> <source srcset="image.jpg" type="image/jpeg"> <img src="image.jpg" alt="Fallback image"> </picture>
Lazy Loading:
// React: Intersection Observer for lazy loading function LazyImage({ src, alt }) { const [isLoaded, setIsLoaded] = useState(false); const [isInView, setIsInView] = useState(false); const imgRef = useRef();
useEffect(() => { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsInView(true); observer.disconnect(); } }, { rootMargin: '50px' } );
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, []);
return ( <div ref={imgRef} className="lazy-image-container"> {!isLoaded && <div className="skeleton-loader" />} {isInView && ( <img src={src} alt={alt} onLoad={() => setIsLoaded(true)} style={{ opacity: isLoaded ? 1 : 0 }} /> )} </div> ); }
Loading Strategies
Skeleton Screens:
function SkeletonCard() { return ( <div className="skeleton-card"> <div className="skeleton skeleton-image" /> <div className="skeleton skeleton-title" /> <div className="skeleton skeleton-text" /> <div className="skeleton skeleton-text short" /> </div> ); }
// CSS .skeleton { background: linear-gradient( 90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75% ); background-size: 200% 100%; animation: loading 1.5s infinite; }
@keyframes loading { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.skeleton-image { height: 200px; border-radius: 8px 8px 0 0; }
.skeleton-title { height: 24px; margin: 16px; border-radius: 4px; }
.skeleton-text { height: 16px; margin: 8px 16px; border-radius: 4px; }
.skeleton-text.short { width: 60%; }
Progressive Web App (PWA):
// service-worker.js const CACHE_NAME = 'mobile-app-v1'; const urlsToCache = [ '/', '/styles/main.css', '/scripts/main.js', '/images/logo.png', ];
self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME) .then((cache) => cache.addAll(urlsToCache)) ); });
self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request) .then((response) => { // Return cached version or fetch new return response || fetch(event.request); }) ); });
Performance Metrics
Core Web Vitals for Mobile:
-
LCP (Largest Contentful Paint): < 2.5s
-
FID (First Input Delay): < 100ms
-
CLS (Cumulative Layout Shift): < 0.1
// Measure performance const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(entry.name, entry.startTime); } });
observer.observe({ entryTypes: ['navigation', 'paint', 'largest-contentful-paint'] });
Responsive Breakpoints
Common Device Widths
/* iPhone SE (2022) / @media (min-width: 375px) and (max-width: 667px) { / Small phone styles */ }
/* iPhone 12/13/14 Pro / @media (min-width: 390px) and (max-width: 844px) { / Standard phone styles */ }
/* iPhone 14 Pro Max / @media (min-width: 428px) and (max-width: 926px) { / Large phone styles */ }
/* iPad Mini / @media (min-width: 768px) and (max-width: 1024px) { / Tablet styles */ }
/* iPad Pro / @media (min-width: 1024px) and (max-width: 1366px) { / Large tablet styles */ }
Orientation-Specific Styles
/* Portrait mode */ @media (orientation: portrait) { .container { flex-direction: column; } }
/* Landscape mode */ @media (orientation: landscape) { .container { flex-direction: row; }
.sidebar { width: 300px; } }
/* Prevent layout shift on keyboard open */ @media (max-height: 500px) { .bottom-nav { display: none; } }
Container Queries (Modern Approach)
/* Component adapts to container size, not viewport */ .card-container { container-type: inline-size; }
@container (min-width: 400px) { .card { display: grid; grid-template-columns: 1fr 2fr; } }
@container (min-width: 600px) { .card { grid-template-columns: 1fr 1fr; } }
Examples
- Mobile E-commerce Product List
function ProductList({ products }) { return ( <div className="product-list"> {products.map((product) => ( <div key={product.id} className="product-card"> <div className="product-image-container"> <img src={product.image} alt={product.name} loading="lazy" /> {product.badge && ( <span className="product-badge">{product.badge}</span> )} </div>
<div className="product-info">
<h3 className="product-name">{product.name}</h3>
<p className="product-price">${product.price}</p>
{product.rating && (
<div className="product-rating">
{'★'.repeat(product.rating)}
{'☆'.repeat(5 - product.rating)}
<span className="review-count">
({product.reviewCount})
</span>
</div>
)}
</div>
<button className="add-to-cart-btn">
Add to Cart
</button>
</div>
))}
</div>
); }
// CSS .product-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; padding: 16px; }
@media (min-width: 768px) { .product-list { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .product-list { grid-template-columns: repeat(4, 1fr); } }
.product-card { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); }
.product-image-container { position: relative; aspect-ratio: 1; background: #f5f5f5; }
.product-image-container img { width: 100%; height: 100%; object-fit: cover; }
.product-badge { position: absolute; top: 8px; right: 8px; background: #FF3B30; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: 600; }
.product-info { padding: 12px; }
.product-name { font-size: 14px; font-weight: 600; margin: 0 0 4px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.product-price { font-size: 16px; font-weight: 700; color: #007AFF; margin: 0 0 8px 0; }
.product-rating { font-size: 14px; color: #FFB800; }
.review-count { color: #666; font-size: 12px; margin-left: 4px; }
.add-to-cart-btn { width: 100%; height: 44px; background: #007AFF; color: white; border: none; font-size: 14px; font-weight: 600; -webkit-tap-highlight-color: transparent; }
.add-to-cart-btn:active { background: #0051D5; }
- Infinite Scroll Feed
function InfiniteFeed() { const [posts, setPosts] = useState([]); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const observerTarget = useRef(null);
const loadMore = async () => { if (loading || !hasMore) return;
setLoading(true);
const newPosts = await fetchPosts(page);
if (newPosts.length === 0) {
setHasMore(false);
} else {
setPosts(prev => [...prev, ...newPosts]);
setPage(prev => prev + 1);
}
setLoading(false);
};
useEffect(() => { const observer = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting) { loadMore(); } }, { threshold: 0.5 } );
if (observerTarget.current) {
observer.observe(observerTarget.current);
}
return () => observer.disconnect();
}, [loading, hasMore]);
return ( <div className="feed"> {posts.map(post => ( <FeedCard key={post.id} post={post} /> ))}
{loading && <LoadingSpinner />}
<div ref={observerTarget} style={{ height: '20px' }} />
{!hasMore && (
<div className="feed-end">No more posts</div>
)}
</div>
); }
- Mobile Search with Autocomplete
function MobileSearch() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); const [isFocused, setIsFocused] = useState(false); const [recentSearches, setRecentSearches] = useState([]);
const handleSearch = async (value) => { setQuery(value);
if (value.length >= 2) {
const searchResults = await fetchSearchResults(value);
setResults(searchResults);
} else {
setResults([]);
}
};
const handleSubmit = (searchQuery) => { // Save to recent searches const updated = [searchQuery, ...recentSearches.slice(0, 4)]; setRecentSearches(updated); localStorage.setItem('recentSearches', JSON.stringify(updated));
// Navigate to results
window.location.href = `/search?q=${encodeURIComponent(searchQuery)}`;
};
return ( <div className="mobile-search"> <div className="search-bar"> <input type="search" inputMode="search" placeholder="Search products..." value={query} onChange={(e) => handleSearch(e.target.value)} onFocus={() => setIsFocused(true)} onBlur={() => setTimeout(() => setIsFocused(false), 200)} />
{query && (
<button
className="clear-button"
onClick={() => {
setQuery('');
setResults([]);
}}
>
✕
</button>
)}
</div>
{isFocused && (
<div className="search-dropdown">
{query.length === 0 && recentSearches.length > 0 && (
<div className="recent-searches">
<h4>Recent Searches</h4>
{recentSearches.map((search, index) => (
<button
key={index}
className="search-suggestion"
onClick={() => handleSubmit(search)}
>
<span className="icon">🕐</span>
{search}
</button>
))}
</div>
)}
{results.length > 0 && (
<div className="search-results">
{results.map((result) => (
<button
key={result.id}
className="search-result-item"
onClick={() => handleSubmit(result.name)}
>
<img src={result.thumbnail} alt="" />
<div>
<div className="result-name">{result.name}</div>
<div className="result-category">{result.category}</div>
</div>
</button>
))}
</div>
)}
</div>
)}
</div>
); }
- Filter Drawer
function FilterDrawer({ isOpen, onClose, onApply }) { const [filters, setFilters] = useState({ priceRange: [0, 1000], category: [], rating: 0, inStock: false, });
return ( <> {isOpen && ( <div className="filter-drawer-overlay" onClick={onClose} /> )}
<div className={`filter-drawer ${isOpen ? 'open' : ''}`}>
<div className="filter-header">
<h2>Filters</h2>
<button onClick={onClose}>✕</button>
</div>
<div className="filter-content">
<div className="filter-section">
<h3>Price Range</h3>
<input
type="range"
min="0"
max="1000"
value={filters.priceRange[1]}
onChange={(e) => setFilters({
...filters,
priceRange: [0, parseInt(e.target.value)]
})}
/>
<div className="price-display">
${filters.priceRange[0]} - ${filters.priceRange[1]}
</div>
</div>
<div className="filter-section">
<h3>Category</h3>
{['Electronics', 'Clothing', 'Books', 'Home'].map(cat => (
<label key={cat} className="checkbox-label">
<input
type="checkbox"
checked={filters.category.includes(cat)}
onChange={(e) => {
if (e.target.checked) {
setFilters({
...filters,
category: [...filters.category, cat]
});
} else {
setFilters({
...filters,
category: filters.category.filter(c => c !== cat)
});
}
}}
/>
{cat}
</label>
))}
</div>
<div className="filter-section">
<label className="checkbox-label">
<input
type="checkbox"
checked={filters.inStock}
onChange={(e) => setFilters({
...filters,
inStock: e.target.checked
})}
/>
In Stock Only
</label>
</div>
</div>
<div className="filter-actions">
<button
className="clear-button"
onClick={() => setFilters({
priceRange: [0, 1000],
category: [],
rating: 0,
inStock: false,
})}
>
Clear All
</button>
<button
className="apply-button"
onClick={() => {
onApply(filters);
onClose();
}}
>
Apply Filters
</button>
</div>
</div>
</>
); }
// CSS .filter-drawer { position: fixed; right: -100%; top: 0; bottom: 0; width: 85%; max-width: 400px; background: white; z-index: 1001; transition: right 0.3s; display: flex; flex-direction: column; }
.filter-drawer.open { right: 0; }
.filter-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #E5E5EA; }
.filter-content { flex: 1; overflow-y: auto; padding: 16px; }
.filter-section { margin-bottom: 24px; }
.filter-section h3 { font-size: 16px; font-weight: 600; margin-bottom: 12px; }
.checkbox-label { display: flex; align-items: center; padding: 12px 0; font-size: 15px; }
.checkbox-label input { margin-right: 12px; width: 20px; height: 20px; }
.filter-actions { display: flex; gap: 12px; padding: 16px; border-top: 1px solid #E5E5EA; }
.clear-button, .apply-button { flex: 1; height: 48px; border-radius: 8px; font-size: 16px; font-weight: 600; }
.clear-button { background: white; border: 2px solid #007AFF; color: #007AFF; }
.apply-button { background: #007AFF; border: none; color: white; }
- Mobile Payment Form
function MobilePaymentForm() { const [cardNumber, setCardNumber] = useState(''); const [expiry, setExpiry] = useState(''); const [cvv, setCvv] = useState('');
const formatCardNumber = (value) => { return value .replace(/\s/g, '') .match(/.{1,4}/g) ?.join(' ') || ''; };
const formatExpiry = (value) => {
const cleaned = value.replace(/\D/g, '');
if (cleaned.length >= 2) {
return ${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)};
}
return cleaned;
};
return ( <form className="payment-form"> <div className="form-group"> <label>Card Number</label> <input type="text" inputMode="numeric" maxLength="19" placeholder="1234 5678 9012 3456" value={formatCardNumber(cardNumber)} onChange={(e) => setCardNumber(e.target.value.replace(/\s/g, ''))} /> </div>
<div className="form-row">
<div className="form-group">
<label>Expiry</label>
<input
type="text"
inputMode="numeric"
maxLength="5"
placeholder="MM/YY"
value={expiry}
onChange={(e) => setExpiry(formatExpiry(e.target.value))}
/>
</div>
<div className="form-group">
<label>CVV</label>
<input
type="text"
inputMode="numeric"
maxLength="4"
placeholder="123"
value={cvv}
onChange={(e) => setCvv(e.target.value.replace(/\D/g, ''))}
/>
</div>
</div>
<button type="submit" className="pay-button">
Pay $99.99
</button>
</form>
); }
6-20. Additional Examples
For brevity, here are summaries of 14 more essential mobile design patterns:
- Sticky Header with Scroll Progress
-
Header shrinks on scroll
-
Progress bar shows reading position
-
Back-to-top button appears after scroll
- Image Gallery with Pinch Zoom
-
Full-screen image viewer
-
Swipe between images
-
Pinch to zoom functionality
- Mobile-Optimized Data Table
-
Horizontal scroll with sticky first column
-
Card view on small screens
-
Expandable rows for details
- Bottom Sheet Menu
-
Swipe up to expand
-
Drag to dismiss
-
Multiple snap points (collapsed, half, full)
- Mobile Calendar Picker
-
Month view optimized for touch
-
Date range selection
-
Quick actions (Today, Tomorrow, Next Week)
- Floating Action Button (FAB) with Speed Dial
-
Primary action always visible
-
Expands to show related actions
-
Smooth animations
- Pull to Refresh
-
Custom loading animation
-
Haptic feedback
-
Success/error states
- Swipeable Tabs
-
Horizontal scroll tabs
-
Active tab indicator
-
Snap to tab on scroll
- Mobile Video Player
-
Custom controls optimized for touch
-
Picture-in-picture mode
-
Gesture controls (tap to pause, double-tap to skip)
- Mobile Toast Notifications
-
Non-intrusive messaging
-
Auto-dismiss with manual override
-
Action buttons
- Collapsible Accordion
-
Touch-friendly expand/collapse
-
Smooth animations
-
Multiple sections
- Mobile Stepper Form
-
Multi-step process
-
Progress indicator
-
Back/Next navigation
- Voice Input Interface
-
Microphone button
-
Real-time transcription
-
Voice feedback
- Onboarding Carousel
-
Swipeable introduction screens
-
Skip option
-
Progress dots
- Mobile Share Sheet
-
Native-like sharing interface
-
Common share targets
-
Copy link functionality
Conclusion
Mobile design requires deep understanding of touch interactions, platform conventions, and performance optimization. By following mobile-first principles, respecting thumb zones, and implementing platform-appropriate patterns, you create experiences that feel natural and performant on mobile devices.
Remember: mobile users are often on-the-go, have limited attention, and expect instant responsiveness. Prioritize speed, clarity, and ease of use above all else.