UI/UX Guide · 30 min read

Mobile App Design Best Practices: UI/UX Guidelines for iOS & Android

Actionable design guidelines for building mobile apps that users love — navigation patterns, typography, accessibility, animations, and the mistakes that kill retention.

iOS & Android guidelines
Real-world examples
Includes accessibility

Why Mobile App Design Matters More Than You Think

The average person has 80 apps installed but uses only 9 regularly. The difference between the apps that make it into daily use and those that get deleted after one session is almost never the feature set — it's the design. Users don't consciously evaluate design; they just experience friction, confusion, or delight, and respond accordingly.

The numbers are stark: 88% of users are less likely to return to an app after a bad user experience (Toptal UX research). The average app loses 77% of its daily active users within the first 3 days after install. At 30 days, 90% of users have churned. Great design directly attacks these numbers — intuitive onboarding reduces day-1 drop-off; fast perceived performance reduces frustration abandonment; clear navigation reduces confusion churn.

This guide covers every major design decision in a mobile app — from the fundamental principles of visual hierarchy and thumb-friendly layout to the specifics of iOS vs Android platform differences, accessibility compliance, and animation timing. Each section includes the reasoning behind the guidelines, not just the guidelines themselves — because understanding the why is the difference between applying rules and making good design decisions.

💡 The Most Important Principle

Design for your actual users, not for yourself. You are not your user. You know your app intimately; your users don't. Every design decision should be validated by watching real people attempt real tasks — even 5 users will surface 85% of major usability issues (Nielsen's law). Design is a hypothesis; user testing is how you find out if you're right.

Core Design Principles

These four principles underpin every good mobile design decision. They apply regardless of platform, app category, or aesthetic direction. Master these and every other design decision becomes more intuitive.

Visual Hierarchy

Guide the user's eye to what matters most

Visual hierarchy is the most fundamental principle in UI design. It controls where a user looks first, second, and third on any screen. Without intentional hierarchy, users scan randomly, miss important information, and feel confused about what to do next.

The most effective tools for creating hierarchy are size, weight, colour, and whitespace — in roughly that order of power. A large, bold element will always draw the eye before a small, light one. Colour used sparingly creates emphasis; used everywhere it creates noise. Whitespace is frequently underused: giving elements room to breathe makes them feel more important and reduces cognitive load significantly.

The 8pt grid system is the practical foundation for consistent hierarchy. Every spacing decision — padding, margins, gaps between elements — should be a multiple of 8 (8, 16, 24, 32, 48, 64). This creates a natural rhythm across the entire app and makes the design feel cohesive even when different screens are built by different developers.

Implementation Guidelines

Apply the 8pt grid: all spacing in multiples of 8px/pt for visual consistency
Use size contrast deliberately — hero text 32–48pt stands clearly above 14–16pt body
Apply the 60-30-10 colour rule: dominant, secondary, accent in those proportions
Use weight contrast (Regular vs Bold) to distinguish content levels without colour
Whitespace is not empty space — it gives hierarchy elements room to breathe

📱 Real-world example: Airbnb's search screen: location field (largest) → date row → guest count → CTA button. Each step is visually smaller than the previous, guiding the user downward.

Thumb-Friendly Design

Design for how people actually hold their phones

Research by Steven Hoober found that 49% of people hold their phone with one hand, using their thumb as the primary input mechanism. The implications for UI layout are significant and frequently ignored.

The thumb naturally reaches a curved arc across the screen. The bottom-centre of the screen is easiest to reach; the top corners are hardest. This creates three distinct zones: the natural zone (bottom third — primary actions go here), the stretch zone (middle — secondary actions), and the difficult zone (top corners — avoid placing anything critical here).

This is why the most successful apps — Instagram, Twitter, TikTok, WhatsApp — all use bottom navigation bars. They put the most frequently used destinations directly in the thumb's natural reach. Navigation bars at the top of the screen (common on desktop web apps that were ported to mobile) force users to constantly stretch or switch to two-handed operation. On larger phones (iPhone 15 Pro Max, Samsung S24 Ultra), the thumb zone shrinks relative to screen size, making bottom-placement even more important.

Implementation Guidelines

Place primary navigation and CTAs in the bottom third of the screen (thumb zone)
Use bottom tab bars for main app sections — never top tabs for primary navigation
Minimum tap target: 44×44pt on iOS, 48×48dp on Android — no exceptions
Leave at least 8pt of space between adjacent tappable elements
Avoid placing critical actions in top corners — they require stretching or two hands

📱 Real-world example: WhatsApp: compose button (bottom-right FAB), tab bar (bottom), chat actions (bottom sheet). Every critical action is reachable with one thumb without repositioning the hand.

Progressive Disclosure

Show only what the user needs, when they need it

Progressive disclosure is the principle of revealing information and options only when they become relevant. Its opposite — showing everything at once — is the most common cause of cluttered, overwhelming interfaces that users abandon.

The principle applies at every scale. At the screen level: don't show advanced settings alongside basic ones on the same screen. At the component level: don't show all filter options in a list when a single 'Filters' button + bottom sheet serves better. At the onboarding level: don't ask for 10 permissions during signup — request them contextually when the feature is first used.

The practical test is to ask: "Does this user need this information right now, at this point in their task?" If the answer is no, defer it. The Uber app is a masterclass in progressive disclosure — you see only a map and a search bar on load; every additional detail (fare estimate, driver info, trip receipt) appears exactly when it becomes relevant.

Implementation Guidelines

Show 3–5 primary actions maximum on any screen; move the rest behind 'More'
Use bottom sheets and modals to reveal secondary options without leaving context
Request permissions contextually at the moment they are needed, not at app launch
In forms, break complex inputs into steps — reveal the next step on completion
Use tooltips and coach marks to teach features the first time, not via text-heavy tutorials

📱 Real-world example: Uber: Home screen shows map + search only. Tap search → origin/destination fields appear. Select destination → fare estimate appears. Accept → driver details appear. Each step reveals only what is relevant.

Perceived Performance

Make the app feel fast even when it isn't

Actual performance (how fast something loads) and perceived performance (how fast it feels) are different things, and perceived performance is what users rate in reviews. You can dramatically improve perceived performance without changing a single line of network or computation code — purely through design decisions.

The most powerful technique is the skeleton screen: instead of a spinner, show a greyed-out placeholder that exactly matches the layout of the real content. The user's brain interprets this as 'almost loaded' rather than 'waiting'. Instagram, Facebook, LinkedIn, and every major app switched to skeleton screens years ago because they measurably reduce perceived wait time by 10–15%.

Optimistic UI is the second major technique: update the UI immediately as if the action succeeded, then roll back silently if the server returns an error. This makes tapping a like button, following a user, or sending a message feel instantaneous. The network call happens in the background. For actions that rarely fail (liking a post, sending a message on good connectivity), this creates a dramatically faster feeling app with no engineering performance work at all.

Implementation Guidelines

Use skeleton screens instead of spinners — match the exact layout of real content
Optimistic UI: update state immediately on tap, roll back silently on server error
Target 60fps (16ms/frame) for all animations — drop to 30fps if needed to hit budget
Lazy-load images below the fold; show low-resolution placeholders with blur-up
App launch must reach first meaningful paint in under 2 seconds on mid-range devices

📱 Real-world example: Twitter / X: When you like a tweet, the heart animates red instantly. The server is called in the background. If it fails (rare), the heart reverts. The experience feels instantaneous even on slow connections.

Touch Targets & Spacing

Touch targets are the single most commonly violated design guideline in mobile apps. Every major platform has clear minimum size requirements, every major accessibility standard references them, and yet apps routinely ship with tap targets half the required size. This happens because designers work on large monitors where everything looks tappable — until you hand the prototype to a user on an actual phone.

The key insight is that the visual size of a button can be smaller than its tap target. A 20×20pt icon can sit inside a 44×44pt transparent tappable area. Users won't see the extra space, but their fingers will benefit from it. This is how all well-designed system UIs work — the back arrow in iOS navigation is visually small but tappable across the entire top-left corner.

StandardMinimum SizeRecommended SizeBetween Targets
iOS (Apple HIG)44 × 44 pt48 × 48 pt8 pt between targets
Android (Material Design)48 × 48 dp56 × 56 dp8 dp between targets
WCAG 2.5.5 (AAA)44 × 44 CSS pxAs large as possibleN/A

⚠️ How to Audit Your App

Enable "Accessibility Inspector" on iOS Simulator (Xcode) or "Touch & Hold Delay" in Android Developer Options to visualise tap areas. In Figma, check if your interactive components have at least 44×44pt frames before handoff. If a developer says 'it looks fine' — test it with one hand, on the street, on a phone with a cracked screen protector. That's the real usage condition.

TouchTarget.tsx — React Native
// ✓ Correct: 20×20pt icon with 44×44pt tap area using hitSlop
<TouchableOpacity
  hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}
  onPress={handleClose}
  accessible={true}
  accessibilityLabel="Close"
  accessibilityRole="button"
>
  <CloseIcon width={20} height={20} />
</TouchableOpacity>

// ✓ Alternative: Pressable with padding + negative margin offset
<Pressable
  style={({ pressed }) => ({
    padding: 12,   // expands tap area to 44×44pt for a 20×20pt icon
    margin: -12,   // offsets padding so surrounding layout is unchanged
    opacity: pressed ? 0.7 : 1,
  })}
  onPress={handleClose}
>
  <CloseIcon width={20} height={20} />
</Pressable>

// ✗ Incorrect: small icon with no expanded tap area
<TouchableOpacity onPress={handleClose}>
  <CloseIcon width={20} height={20} />   {/* tap target = 20×20pt — fails HIG */}
</TouchableOpacity>

Color & Typography

Colour and typography together account for more of the app's visual character than any other design element. Used deliberately, they create a coherent brand identity and guide users through the interface. Used carelessly, they create confusion, fatigue, and accessibility failures.

Colour Principles

The 60-30-10 Rule

Your app's colour palette should follow a strict ratio. 60% of the UI uses your dominant colour (usually white, off-white, or dark gray for background surfaces). 30% uses your secondary colour (card backgrounds, section dividers, secondary text). 10% uses your accent colour (CTAs, active states, links, highlights). This ratio mirrors how professional interior designers approach room colour and creates the same effect: a cohesive space where accent colours feel deliberate and eye-catching rather than overwhelming.

Contrast Ratios Are Not Optional

WCAG AA requires 4.5:1 contrast ratio for normal text and 3:1 for large text (18pt+ or 14pt+ bold). WCAG AAA requires 7:1 for normal text. These are not just accessibility guidelines — they're usability guidelines. Low contrast text is hard to read in bright sunlight (where most mobile usage happens) for everyone, not just users with visual impairments. Use a contrast checker (WebAIM, Figma's built-in checker) on every text/background combination before shipping.

Dark Mode Is Now Expected

As of 2024, dark mode is not a bonus feature — it's expected by users and required by Apple's App Store guidelines. Design your colour system with both light and dark variants from the start, not as an afterthought. Use semantic colour tokens (background-primary, text-secondary, accent) rather than hard-coded hex values. This makes dark mode a matter of swapping token sets rather than redesigning every screen.

Colour Blindness Affects 8% of Men

Red-green colour blindness (deuteranopia) is by far the most common, affecting approximately 8% of male users. Never use colour as the only way to communicate meaning. If you show errors in red and success in green, also use icons (✗ and ✓), text labels, and different layouts. A simple rule: if your UI would be confusing in greyscale, it relies too much on colour.

design-token-architecture.txt

  DESIGN TOKEN ARCHITECTURE — from raw values to components
  ──────────────────────────────────────────────────────────

  ┌─────────────────────────────────────────────┐
  │           PRIMITIVE TOKENS                  │
  │   (raw values — never reference directly)   │
  │                                             │
  │   amber-400: #FBBF24   gray-950: #030712    │
  │   amber-500: #F59E0B   gray-50:  #F9FAFB    │
  └──────────────────┬──────────────────────────┘
                     │
         ┌───────────┴────────────┐
         │                        │
  ┌──────▼───────┐       ┌────────▼────────┐
  │ SEMANTIC     │       │  SEMANTIC       │
  │ LIGHT TOKENS │       │  DARK TOKENS    │
  │              │       │                 │
  │ bg: white    │       │ bg: gray-950    │
  │ surface: 50  │       │ surface: #1F293 │
  │ text: gray-9 │       │ text: gray-50   │
  │ accent: 500  │       │ accent: 400     │
  └──────┬───────┘       └────────┬────────┘
         │                        │
         └───────────┬────────────┘
                     │  (swap at runtime via useColorScheme)
  ┌──────────────────▼──────────────────────────┐
  │           COMPONENT STYLES                  │
  │                                             │
  │   button-bg    →  tokens.accent             │
  │   card-bg      →  tokens.surface            │
  │   primary-text →  tokens.textPrimary        │
  │   border       →  tokens.border             │
  └─────────────────────────────────────────────┘

  Rule: components never reference primitive tokens directly.
        Only semantic tokens flow into component stylesheets.
colors.ts — semantic token system
// ✓ Semantic token approach — zero component changes needed for dark mode
const palette = {
  amber400: '#FBBF24', amber500: '#F59E0B',
  gray50:   '#F9FAFB', gray900:  '#111827', gray950: '#030712',
};

export const tokens = {
  light: {
    background:    '#FFFFFF',
    surface:       palette.gray50,
    textPrimary:   palette.gray900,
    textSecondary: '#6B7280',
    accent:        palette.amber500,
    border:        '#E5E7EB',
  },
  dark: {
    background:    palette.gray950,
    surface:       '#1F2937',
    textPrimary:   palette.gray50,
    textSecondary: '#9CA3AF',
    accent:        palette.amber400,  // slightly lighter for dark backgrounds
    border:        '#374151',
  },
};

// In React Native: one hook, automatic light/dark switching
import { useColorScheme } from 'react-native';

export function useTheme() {
  const scheme = useColorScheme(); // 'light' | 'dark' | null
  return tokens[scheme ?? 'light'];
}

// Usage in components — identical code, correct colours in both modes
function Card({ title, subtitle }: CardProps) {
  const theme = useTheme();
  return (
    <View style={{ backgroundColor: theme.surface, borderColor: theme.border }}>
      <Text style={{ color: theme.textPrimary }}>{title}</Text>
      <Text style={{ color: theme.textSecondary }}>{subtitle}</Text>
    </View>
  );
}

// ✗ Incorrect: hardcoded hex values require a full redesign for dark mode
<View style={{ backgroundColor: '#FFFFFF' }}>
  <Text style={{ color: '#111827' }}>Content</Text>
</View>

Typography Principles

Use the Platform's System Font

San Francisco (iOS) and Roboto (Android) are the system fonts for their respective platforms. They are optimised for screen rendering at every size, support all system-level accessibility features (Dynamic Type, bold text), and feel native to the platform. Custom fonts are appropriate for brand headlines and marketing moments, but body text and UI labels should almost always use the system font.

Minimum Body Text Size: 14pt

14pt is the absolute minimum for any text a user needs to read. Smaller than 14pt should be used only for metadata, timestamps, and captions — content that provides context rather than information the user needs to actively read. Many designers set body text at 16pt, which reduces eye strain significantly on long-read screens. Never use text smaller than 11pt for any purpose.

Line Height and Letter Spacing

Body text should have a line height of 1.4–1.6× the font size. At 16pt font, that's 22–26pt line height. This spacing prevents lines from feeling cramped and makes long paragraphs scannable. For headlines, tighter line height (1.1–1.2×) looks intentional and bold. Letter spacing (tracking) should generally be 0 for body text and slightly negative for large headlines — this is how type designers set it by default in modern fonts.

Type Scale Reference

LevelSizeWeightLine HeightUse For
Hero / Display32–48ptBold (700)1.1×App name, marketing splash screens, major empty states
H1 — Page Title28–32ptBold (700)1.2×Screen titles, section openers
H2 — Section Header22–24ptSemibold (600)1.3×Card titles, section headings within a screen
H3 — Subsection18–20ptSemibold (600)1.3×List item titles, grouped section labels
Body — Primary16ptRegular (400)1.5×Main content, descriptions, article text
Body — Secondary14ptRegular (400)1.5×Supporting information, secondary descriptions
Caption / Label12–13ptRegular / Medium1.4×Timestamps, metadata, input labels, badge text

Accessibility Guidelines

Accessibility is frequently treated as a compliance checkbox rather than what it actually is: a quality signal. Apps that are accessible are better designed for everyone — high-contrast text is easier to read in bright sunlight, larger tap targets are easier for any finger, clear error messages help any user. The accessibility audit is the best usability audit.

In practical terms: Apple requires apps to support VoiceOver to be featured in the App Store, and Google has similar requirements for accessibility on Google Play. Beyond compliance, users with disabilities are a significant audience — approximately 1 in 5 people worldwide has some form of disability — and they are loyal to apps that serve them.

Visual Accessibility

Scope: ~15% of users have some form of visual impairment

Visual accessibility is not just about blind users using screen readers. It encompasses low vision, colour blindness, and situational impairments like bright sunlight or a scratched screen. The single most impactful thing you can do is ensure sufficient contrast — it benefits every user, not just those with impairments. Dynamic Type support (iOS) and font scaling (Android) allow users to set their preferred text size system-wide; if your layout breaks at large sizes, it fails a significant portion of your user base.

  • 4.5:1 contrast ratio for body text, 3:1 for large text (18pt+ regular or 14pt+ bold)
  • Never use colour alone to convey meaning — always pair with an icon, label, or pattern
  • Support Dynamic Type on iOS and sp-based font sizing on Android so text scales correctly
  • All images and icons that convey meaning must have descriptive alt text / content descriptions
  • Don't rely on subtle distinctions (light gray vs lighter gray) — use clear visual differentiation

Motor Accessibility

Scope: ~7% of adults have difficulty with fine motor control

Motor accessibility often gets overlooked because it seems like an edge case. But consider that a user might be holding a handrail on a train, carrying groceries, or simply have hands that aren't as nimble as a 25-year-old designer's. The 44×44pt minimum tap target rule exists because of research on average finger pad contact area. A 30×30pt button is functional for many users but fails for a meaningful segment — and it fails for all users when their hands are cold, wet, or moving. Larger targets with clear visual affordance are always better.

  • Every tappable element: minimum 44×44pt (iOS) / 48×48dp (Android), even if visually smaller
  • Minimum 8pt spacing between adjacent tap targets to prevent mis-taps
  • All gestures (swipe, pinch) must have a button alternative — not everyone can gesture precisely
  • Support Switch Control (iOS) and Switch Access (Android) for users who can't touch the screen
  • Avoid time-limited interactions — if a toast disappears after 2 seconds, some users can't read it

Cognitive Accessibility

Scope: Improves the experience for every single user

Cognitive accessibility is the most universally beneficial category because it addresses how the brain processes information — something that affects everyone, not just users with cognitive disabilities. The principles overlap heavily with general UX best practices: clear language, predictable patterns, helpful error messages, and manageable information density. An app that is cognitively accessible is simply a well-designed app. The plain language principle is particularly important: if your error message requires technical knowledge to understand, most users cannot act on it.

  • Write at an 8th-grade reading level — avoid jargon, passive voice, and ambiguity
  • Error messages must explain what went wrong AND what the user should do next
  • Keep navigation consistent — the back button should always go back, the tab bar never disappears
  • Break multi-step tasks into clearly numbered steps with a visible progress indicator
  • Provide undo for destructive actions — deletion, account changes, sending messages

Screen Reader Support

Scope: Required for App Store compliance in many regions

VoiceOver (iOS) and TalkBack (Android) are screen readers used by blind and low-vision users. They announce UI elements based on accessibility labels and traits you set in code. A button that visually shows an X icon to close a modal is silent to a screen reader unless you provide an accessibility label like 'Close modal'. This is entirely a developer responsibility but must be planned for in design: every icon-only element, every custom component, and every animated state change must have a corresponding screen reader announcement designed.

  • Every icon-only button needs an accessibility label describing its action, not its appearance
  • Group related elements so screen readers announce them as a unit (e.g., 'John Smith, 3 messages, 2 minutes ago')
  • Announce dynamic content changes — if a badge count updates, the screen reader should announce it
  • Test with VoiceOver / TalkBack before every release — even 10 minutes catches major issues
  • Mark decorative images as hidden from accessibility (they add noise for screen reader users)
Accessibility.tsx — React Native
// ✓ Icon-only button: describe the action, not the visual
<TouchableOpacity
  accessible={true}
  accessibilityLabel="Add to favourites"   // NOT "Star icon"
  accessibilityRole="button"
  accessibilityState={{ selected: isFavourited }}
  accessibilityHint="Double-tap to toggle favourite"
  onPress={toggleFavourite}
>
  <StarIcon filled={isFavourited} />
</TouchableOpacity>

// ✓ Group related elements into one accessibility unit
// Screen reader announces: "John Smith, 3 unread messages, 2 minutes ago"
<View
  accessible={true}
  accessibilityLabel={user.name + ', ' + msgCount + ' unread, ' + timeAgo}
>
  <Avatar source={user.avatar} />
  <Text>{user.name}</Text>
  <Text>{msgCount} messages</Text>
  <Text>{timeAgo}</Text>
</View>

// ✓ Announce dynamic content changes (notification badge, live feed)
import { AccessibilityInfo } from 'react-native';

function onNewMessages(count: number) {
  AccessibilityInfo.announceForAccessibility(
    count + ' new message' + (count !== 1 ? 's' : '')
  );
}

// ✗ Decorative image exposed to screen readers (announces file name)
<Image source={bannerImage} />

// ✓ Hide decorative images — they add noise for screen reader users
<Image source={bannerImage} accessibilityElementsHidden={true} />

iOS vs Android: Platform Differences

iOS and Android are not the same platform with different screen sizes. They have different navigation models, different interaction patterns, different icon systems, and different user expectations built up from years of platform-specific app usage. An app that looks identical on both platforms is almost certainly wrong on at least one.

The most important platform difference is navigation: iOS has no system-level back button — users rely on the app's UI back button or the left-edge swipe gesture. Android has a system back gesture or button that users can always use. This means an Android app can have no visible back button and work fine; an iOS app without a back button traps the user. Design your navigation system accounting for this fundamental difference.

AspectiOS (Apple HIG)Android (Material Design 3)Key Difference
NavigationBottom tab bar, back swipe from left edge, modal sheets slide up from bottomBottom nav bar, system back button/gesture, back stack managed by OSAndroid has a system-level back navigation that iOS lacks — never remove Android's back functionality
TypographySan Francisco (SF Pro), Dynamic Type, large title style on scrollRoboto / Noto, sp-based scaling, collapsing toolbar patternsiOS large titles animate on scroll automatically; Android uses CollapsingToolbarLayout
Buttons & CTAsRounded rectangles, minimal borders, filled for primary actionsFilled, outlined, text button — Material Design hierarchy strictly definedAndroid's Material Design has more explicit button hierarchy rules than iOS HIG
Menus & OverflowContext menus, action sheets (bottom), long-press for optionsOverflow menu (⋮ top-right), bottom sheets, long-press for multi-selectThe ⋮ overflow menu is an Android pattern — don't use it on iOS
Status Bar & Safe AreasDynamic Island / notch at top, Home Indicator at bottom — use safe area insetsVaried notch/punch-hole designs, gesture navigation bar at bottomBoth platforms have safe area APIs — always respect them or UI gets clipped
IconographySF Symbols — 6,000+ icons that adapt to Dynamic Type and weightMaterial Symbols — similar scale, also adaptiveDon't use SF Symbols on Android or Material icons on iOS — they feel foreign

🔑 The Most Practical Approach

If using React Native or Flutter, use platform-adaptive components where they exist (CupertinoNavigationBar on iOS, AppBar on Android). For custom components, create two variants and switch based on platform. The areas where platform-specific design matters most: navigation bars, back navigation, modals, and status bar handling. Everything else can often be shared safely.

PlatformAdapter.tsx — React Native
import { Platform, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

// Platform.select: apply different values per platform in StyleSheet
const cardStyles = StyleSheet.create({
  shadow: {
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.08,
        shadowRadius: 8,
      },
      android: {
        elevation: 4,              // Android uses elevation, not shadow
        backgroundColor: '#FFF',   // Required — elevation needs a bg colour
      },
    }),
  },
});

// Platform.OS: conditionally render platform-specific UI elements
const BackButton = () => {
  // Android has a system back gesture — no in-app back button needed
  if (Platform.OS === 'android') return null;

  return (
    <TouchableOpacity onPress={navigation.goBack} hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }}>
      <ChevronLeftIcon width={24} height={24} />
    </TouchableOpacity>
  );
};

// Safe area insets: always respect Dynamic Island, notch, and gesture bar
function BottomTabBar() {
  const insets = useSafeAreaInsets();

  return (
    <View style={{
      paddingBottom: Math.max(insets.bottom, 8), // min 8pt even on flat-bottom phones
      paddingLeft:  insets.left,
      paddingRight: insets.right,
    }}>
      {/* Tab items */}
    </View>
  );
}

Animation & Micro-interactions

Animation in UI is not decoration — it is communication. Motion tells users what happened (feedback), where they're going (spatial orientation), and what they should focus on (attention direction). The difference between animations that feel right and animations that feel wrong almost always comes down to two things: duration and easing curve.

Duration: most UI animations should be between 100ms and 500ms. Faster than 100ms and users don't register the animation happened. Slower than 500ms and users feel they're waiting. The exception is loading animations, which loop continuously. Easing: physical objects don't start and stop abruptly. Ease-out (starts fast, ends slow) feels natural for most UI elements entering the screen. Spring animations feel particularly satisfying for elements that overshoot and settle — they mimic real physical objects.

Micro-interactions

100–300msSpring or ease-out

Micro-interactions are the tiny animations that provide immediate feedback to user input — a button that scales slightly on press, a checkbox that animates its checkmark, a heart that fills with colour when tapped. They serve a functional purpose: they confirm that the system received the user's input. Without them, users often tap repeatedly thinking nothing happened. Keep them fast (under 300ms) and subtle — the user should feel a response, not watch an animation.

Button press scale (0.95× for 150ms)Like/favourite fill animationToggle switch slidePull-to-refresh bounce

Rule: If the user has to wait for the animation to finish before they can do something else, it's too slow.

Screen Transitions

300–400msEase-in-out (cubic-bezier(0.4, 0, 0.2, 1))

Screen transitions communicate spatial relationships between screens. When you push a detail view, it slides in from the right — teaching the user they can go back by swiping left. When a modal appears, it slides up from the bottom — teaching the user they can dismiss it by swiping down. These directional cues are not decorative; they build a mental model of the app's structure. The duration should be long enough to feel smooth but short enough that the user isn't waiting. 300–400ms is the research-validated sweet spot.

Push navigation (right → left)Modal presentation (bottom → up)Tab switch (cross-fade or slide)Card expansion (origin-aware scale)

Rule: Transition direction should always reflect the spatial relationship — going deeper pushes right, going back pulls left.

Loading & Skeleton Screens

Continuous (1.5–2s shimmer loop)Linear (shimmer), Ease-out (content reveal)

Loading states are one of the most psychologically important design decisions in a mobile app. A blank white screen while data loads creates anxiety — the user doesn't know if the app is working. A spinner reduces anxiety slightly but provides no information about what is loading. A skeleton screen — a greyed-out placeholder that matches the exact shape and layout of the real content — dramatically reduces perceived wait time because the user's brain interprets it as 'almost done'. The shimmer animation that moves across skeleton screens further reinforces activity. Always use skeleton screens for content that loads in under 5 seconds; for longer loads, show a progress indicator with an estimate.

Content skeleton (card-shaped placeholders)Shimmer animation (left-to-right gradient sweep)Progressive image loading (blur-up technique)Incremental list rendering

Rule: Skeleton screens should match the real layout exactly — a skeleton that looks nothing like the loaded content is confusing.

Success & Error Celebrations

400–800msSpring (overshoot for celebration)

Moments of success — completing a task, unlocking an achievement, making a purchase — deserve acknowledgement. A well-designed success animation creates a positive emotional response that increases user satisfaction and return rate. Duolingo's character animations, Headspace's breathing animations, and PayPal's payment confirmation checkmark are all examples of success animations that users describe positively in reviews. Use these sparingly — they should feel special, not routine. Error states should use attention-grabbing but not alarming animations (a gentle shake, a red pulse) that communicate 'something needs your attention' without being stressful.

Payment confirmation checkmark + confettiAchievement badge unlock with scale + glowForm validation error shake (±6pt, 3 times)Progress completion particle burst

Rule: Success animations should feel rewarding; error animations should feel helpful, not punishing.

LikeButton.tsx — spring animation + optimistic UI
import { Animated, Easing, TouchableWithoutFeedback } from 'react-native';

function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  const scale = useRef(new Animated.Value(1)).current;

  const handlePress = () => {
    // Optimistic UI: update state immediately, sync to API in background
    setLiked(prev => !prev);
    api.toggleLike(postId).catch(() => setLiked(prev => !prev)); // revert on failure

    // Spring sequence: compress → overshoot → settle (≈ 400ms total)
    Animated.sequence([
      Animated.timing(scale, {
        toValue: 0.82,
        duration: 80,
        easing: Easing.out(Easing.quad),
        useNativeDriver: true,
      }),
      Animated.spring(scale, {
        toValue: 1.25,
        friction: 3,    // low friction = more overshoot bounce
        tension: 120,
        useNativeDriver: true,
      }),
      Animated.spring(scale, {
        toValue: 1,
        friction: 6,
        tension: 100,
        useNativeDriver: true,
      }),
    ]).start();
  };

  return (
    <TouchableWithoutFeedback
      onPress={handlePress}
      accessible={true}
      accessibilityLabel={liked ? 'Remove like' : 'Like'}
      accessibilityRole="button"
    >
      <Animated.View style={{ transform: [{ scale }] }}>
        <HeartIcon filled={liked} color={liked ? '#EF4444' : '#9CA3AF'} size={24} />
      </Animated.View>
    </TouchableWithoutFeedback>
  );
}

// For CSS/React Native Web: equivalent using CSS transforms
// .like-button:active { transform: scale(0.82); }
// .like-button.liked  { animation: heartBounce 400ms ease forwards; }
// @keyframes heartBounce {
//   0%   { transform: scale(1);    }
//   30%  { transform: scale(0.82); }
//   60%  { transform: scale(1.25); }
//   100% { transform: scale(1);    }
// }

Loading & Empty States

Loading states and empty states are two of the most neglected areas in mobile design. Most teams design the "happy path" — fully loaded, content-rich screens — and leave loading and empty states as developer decisions. This is a mistake. These states are often a user's first impression of the app, and they directly affect whether that user decides to keep using it or leave.

Initial LoadRecommended pattern: Skeleton Screen

When the app opens and the home screen data hasn't loaded yet, show a skeleton that exactly matches the layout — same card dimensions, same avatar circle position, same text line widths. The user's brain pre-renders the content into the skeleton, making the reveal feel like 'filling in' rather than 'appearing from nothing'.

Don't: Never show a full-screen spinner. Never show a blank white screen.

Refreshing Existing ContentRecommended pattern: Pull-to-Refresh + Subtle Indicator

When the user pulls to refresh, show the platform-standard pull-to-refresh indicator (iOS spinner, Android circular indicator). Once content is loaded, update it in-place without a full-screen reload. If new items were added at the top, show a 'X new items' chip that the user can tap to scroll up and see them — this is the Twitter/Instagram pattern that prevents disorienting the user.

Don't: Never clear the existing content before new content is ready — this creates a jarring blank state.

Empty State (No Content)Recommended pattern: Illustrated Empty State with CTA

Empty states — first-time users with no data, search with no results, filtered list with no matches — are frequently neglected and left as plain 'No results found' text. A well-designed empty state explains why it's empty, shows a friendly illustration, and provides a clear CTA to add content or change the filter. This is the difference between users understanding the situation and users assuming the app is broken.

Don't: Never just show 'No results'. Always explain why and what to do next.

Error StateRecommended pattern: Error Illustration + Retry Action

Error states must include: what went wrong (in plain language), whether it's permanent or temporary, and what the user can do. 'An error occurred' is useless. 'Can't connect to the internet — check your connection and try again' with a Retry button is useful. For server errors the user can't fix, acknowledge that it's not their fault and offer to notify them when it's fixed or try later.

Don't: Never show raw error codes or technical messages to end users.

FeedSkeleton.tsx — shimmer loading state
import { Animated, Easing, View, StyleSheet } from 'react-native';

// Reusable shimmer hook — share one animation loop across all skeleton elements
function useShimmer() {
  const shimmer = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    Animated.loop(
      Animated.timing(shimmer, {
        toValue: 1,
        duration: 1400,
        easing: Easing.linear,
        useNativeDriver: true,
      })
    ).start();
    return () => shimmer.stopAnimation();
  }, []);

  return shimmer.interpolate({
    inputRange:  [0, 1],
    outputRange: [-300, 300],  // sweeps left → right across the element
  });
}

// Skeleton card that EXACTLY mirrors the real FeedCard layout
// Same dimensions, same positions — the brain reads it as "almost loaded"
function FeedCardSkeleton() {
  const translateX = useShimmer();

  const shimmerStyle = {
    transform: [{ translateX }],
    position:  'absolute' as const,
    width:     '200%',
    height:    '100%',
    backgroundColor: 'rgba(255,255,255,0.18)',
  };

  return (
    <View style={s.card}>
      {/* Avatar placeholder — same 48×48pt as real avatar */}
      <View style={s.avatar}>
        <Animated.View style={shimmerStyle} />
      </View>

      <View style={s.body}>
        {/* Name line: 60% width ≈ average display name length */}
        <View style={[s.line, { width: '60%' }]}>
          <Animated.View style={shimmerStyle} />
        </View>
        {/* Content line: 90% width ≈ one line of body text */}
        <View style={[s.line, { width: '90%', marginTop: 8 }]}>
          <Animated.View style={shimmerStyle} />
        </View>
        {/* Second content line: 75% width */}
        <View style={[s.line, { width: '75%', marginTop: 6 }]}>
          <Animated.View style={shimmerStyle} />
        </View>
      </View>
    </View>
  );
}

const s = StyleSheet.create({
  card:   { flexDirection: 'row', padding: 16, overflow: 'hidden' },
  avatar: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#E5E7EB', overflow: 'hidden' },
  body:   { flex: 1, marginLeft: 12, justifyContent: 'center' },
  line:   { height: 14, borderRadius: 4, backgroundColor: '#E5E7EB', overflow: 'hidden' },
});

// Usage: render skeletons while data is loading, swap to real content when ready
function FeedScreen() {
  const { data, isLoading } = useFeed();
  return isLoading
    ? Array.from({ length: 6 }).map((_, i) => <FeedCardSkeleton key={i} />)
    : data.map(item => <FeedCard key={item.id} item={item} />);
}

Common Design Mistakes That Kill Retention

These mistakes appear in the majority of first-version mobile apps. They are the most common causes of negative App Store reviews, high day-1 churn, and support tickets. Knowing them before you start is significantly cheaper than fixing them after launch.

Cluttered Interface — Showing Everything at Once

Critical

Placing 8–10 buttons, multiple CTAs, and dense information on a single screen. This forces users to read the entire screen to find what they need, dramatically increasing cognitive load and time-to-action.

Consequence

Increased bounce rate, user frustration, and support tickets asking 'how do I do X' when the button is right there — just buried.

The Fix

Apply progressive disclosure. Identify the one primary action for each screen and make it visually dominant. Secondary actions go behind a 'More' option, overflow menu, or secondary screen. A good rule: if you can't explain the purpose of a screen in one sentence, it has too many jobs.

Bad: Home screen with 12 navigation options. Good: 3 bottom tabs + search for everything else.

Ignoring Platform Conventions

High

Using iOS navigation patterns on Android (or vice versa). Placing back buttons top-left on Android, using the ⋮ overflow menu on iOS, using Android-style cards on iOS. Each platform has established conventions that users have learned over years of using other apps.

Consequence

Users immediately feel 'something is off' even if they can't articulate why. The app feels foreign and low quality. Friction increases for every platform-specific interaction.

The Fix

Build separate UI layers for iOS and Android, or use Flutter/React Native with platform-adaptive components. Never simply port a web layout to mobile — the interaction models are fundamentally different. Read Apple's HIG and Google's Material Design documentation before starting.

Bad: Left-positioned back button visible on Android (users expect system back). Good: Use Android's system back gesture, show no custom back button.

Small Tap Targets

High

Interactive elements smaller than 44×44pt (iOS) or 48×48dp (Android). This is one of the most common mistakes in apps designed on desktop, where clicking is more precise than tapping.

Consequence

Mis-taps, user frustration, accidental navigation, and accessibility failures. On larger phones, small targets in corners are nearly impossible to hit one-handed. Apps with this problem consistently get reviews mentioning 'hard to use'.

The Fix

The visual size of a button can be smaller than its tap target. Use padding to extend the tappable area without changing the visual design. In code, add hit slop (React Native) or increase the touch delegate area. Test every interactive element on a real device with one hand.

Bad: 32×32pt close button visually and functionally. Good: 24×24pt icon inside a 44×44pt tappable area with transparent padding.

No Loading or Error States

Critical

Showing a blank screen, frozen UI, or raw error text while data loads or when something fails. This affects every screen that makes a network call — which is most screens in most apps.

Consequence

Users assume the app is broken and close it. 88% of users say they wouldn't return to an app after a bad experience. Network-related crashes in otherwise functional apps are a leading cause of negative App Store reviews.

The Fix

Every screen that fetches data needs three states designed: loading (skeleton or spinner), success (real content), and error (error state with retry). This is not optional. Design all three before handing off to developers — if you only design the success state, you will get either nothing or a spinner for the other two.

Bad: White screen for 3 seconds while feed loads. Good: Skeleton screen matching feed layout → real content fades in.

Inconsistent Spacing and Alignment

Medium

Random spacing values (13px here, 17px there), elements that are almost-but-not-quite aligned, different padding inside similar components. This is almost always caused by designing without a spacing system.

Consequence

The app looks unpolished even if individual elements look good. Users don't consciously notice inconsistent spacing but they do notice that something 'feels off'. Developers make different decisions when spacing isn't specified, creating visible inconsistencies.

The Fix

Adopt the 8pt grid system from the start. All spacing values must be multiples of 4 or 8 (4, 8, 12, 16, 24, 32, 48, 64). Use auto-layout in Figma with consistent gap values. Document spacing as design tokens so developers use exact values, not estimates.

Bad: Padding inside cards varies from 12px to 16px to 20px. Good: All cards use 16px padding, all card gaps are 8px.

Forgetting About the Keyboard

Critical

On iOS and Android, the software keyboard takes up 40–50% of the screen when it appears. If form fields, CTAs, and error messages aren't repositioned above the keyboard, they become invisible and users cannot complete the form.

Consequence

Users abandon sign-up, checkout, and login flows. This is a particularly expensive mistake — these are often the highest-value screens in any app.

The Fix

Always test every form and text input with the keyboard visible on a real device. Use KeyboardAvoidingView (React Native), windowSoftInputMode adjustResize (Android), or proper SwiftUI padding to ensure the active field and the submit button are always visible above the keyboard.

Bad: 'Create Account' button hidden behind keyboard on sign-up screen. Good: Form scrolls up, CTA stays pinned above keyboard.

Design Tools

The right tools for mobile design depend on your workflow, team size, and what you're trying to validate. Figma handles 90% of needs for most teams. The others fill specific gaps — advanced animation, user testing, analytics.

UI/UX Design

Figma has become the industry standard for mobile UI design. It's collaborative, has excellent mobile-specific features (auto-layout, component variants, prototype modes), and exports assets in every format needed by iOS and Android developers. Sketch remains relevant for Mac-only teams with existing Sketch libraries.

FigmaUse this

Industry standard — collaborative, component-driven, real-time multiplayer

SketchIf your team is Mac-only

Mac-only, excellent plugin ecosystem, strong design system support

FramerFor advanced prototyping

Design + code export, excellent for interactive prototypes with real data

Adobe XDOnly if locked into Adobe

Adobe ecosystem integration, declining market share

Prototyping

High-fidelity prototypes are essential for validating navigation flows and micro-interactions before development begins. Figma's built-in prototyping covers most needs. ProtoPie handles complex conditional logic and sensor-based interactions that Figma can't. Use the right level of fidelity for the question you're trying to answer — a paper sketch validates information architecture; a ProtoPie prototype validates complex interactions.

Figma PrototypeStart here

Screen flows, basic transitions, user testing of layouts

ProtoPieFor complex flows

Conditional logic, sensor interactions, advanced animations

PrincipleFor animation-heavy work

Micro-interaction design, animation timing, beautiful demos

FramerFor realistic data testing

Code-backed prototypes with real API data

User Testing

No amount of design expertise replaces watching real users use your app. Schedule user testing sessions before major releases — even 5 users will surface 85% of major usability issues (Nielsen's law). Remote testing tools have made this accessible to any team and any budget.

UserTesting.comBest for broad samples

Remote moderated and unmoderated testing with panel recruitment

MazeBest for design validation

Rapid prototype testing, task completion rates, click heatmaps

LookbackBest for moderated research

Live user interviews, session recording with observer rooms

HotjarBest for post-launch insights

Session recordings and heatmaps for shipped apps

Analytics

Design decisions should be validated with data. Event tracking tells you where users drop off in flows; funnels show conversion rates; retention charts show if the design keeps users coming back. Set up analytics before launch, not after — you need a baseline to measure improvements against.

MixpanelBest for product analytics

Event tracking, funnel analysis, cohort retention — best-in-class UX

AmplitudeStrong Mixpanel alternative

Behavioural analytics, powerful segmentation, similar to Mixpanel

Firebase AnalyticsBest for small budgets

Free, deep Google integration, A/B testing built-in

FullStoryBest for debugging UX issues

Session replay on real devices, rage-tap detection

Frequently Asked Questions

Detailed answers to the design questions we most frequently receive from product teams building their first mobile app or redesigning an existing one.

Q.Should I design for iOS or Android first?

Design for the platform that represents the majority of your target audience — check industry data for your app category. If it's genuinely split, start with iOS because it has stricter design guidelines (Apple's HIG is more prescriptive than Material Design) and fewer device screen size variations to account for. Once the iOS design is solid, adapt it for Android by applying Material Design components and switching to Android navigation patterns. The critical thing is to never just use the iOS design as-is on Android — the platform conventions are different enough that a direct port will feel wrong to Android users.

Q.How many screens should an onboarding flow have?

As few as possible. Research consistently shows onboarding completion rates drop sharply after 3 screens. The best onboarding doesn't front-load information — it teaches users by letting them do the thing immediately. Consider showing only 1–2 screens that communicate your core value proposition, then get users into the app. Teach features contextually with tooltips and coach marks when the user first encounters them, not upfront. Only ask for permissions (notifications, location, camera) at the exact moment they're needed, not during onboarding. Duolingo's 'just start a lesson immediately' approach is the gold standard — zero onboarding screens.

Q.What's the most common reason apps get rejected in accessibility audits?

By a significant margin: insufficient colour contrast and missing accessibility labels on icon-only buttons. Many teams design with contrast ratios that look fine on calibrated studio monitors but fail on average phone screens in daylight. Run every text/background combination through a WCAG contrast checker (aim for AA minimum, AAA where practical). For icon-only elements — the most common being close buttons, navigation icons, and action icons — every single one must have an accessibility label that describes its action. 'Close' not 'X'. 'Add to favourites' not 'Star'. Test with VoiceOver / TalkBack before every release.

Q.How do I design for different screen sizes without duplicating work?

Use auto-layout in Figma from the start — design components that stretch, stack, and reflow based on available space. On the development side, use relative sizing units (percentage widths, flex layouts) rather than fixed pixel dimensions. Design for three representative sizes: small (iPhone SE / small Android, ~375px wide), standard (iPhone 15 / mid-range Android, ~390px wide), and large (iPhone 15 Pro Max / large Android, ~430px+ wide). Test these three sizes in your prototype tool and on real devices. If your layout holds up across these three sizes, it will hold up across the full range. Pay special attention to text wrapping, button stacking, and bottom navigation safe areas.

Q.When should I use a bottom sheet vs a modal vs a new screen?

Use a new screen (push navigation) when the user is going deeper into a content hierarchy — from a list into a detail view, from settings into a specific setting. Use a modal (full-screen) when the user needs to complete a focused task that is separate from their current context — like composing a new message or completing a form that applies globally. Use a bottom sheet when you need to show supplementary options or details without fully leaving the current screen — like a share menu, filter options, or a quick preview. Bottom sheets are dismissible by swiping down, which feels natural and non-committal. The rule: if dismissing it with a swipe makes sense, use a bottom sheet; if the user must take an explicit action to proceed, use a modal.

Q.How do I decide between dark mode support being optional vs required?

For any consumer app targeting iOS or Android, dark mode support is effectively required. Apple's App Store guidelines don't mandate it technically, but iOS defaults to dark mode for many users and UI that doesn't support it looks jarring — white screens inside a dark system UI. More importantly, 81% of smartphone users say they use dark mode 'most of the time' or 'always' (Android Authority, 2023). Build your colour system with semantic tokens from the start (backgroundColor-primary, textColor-secondary) so switching between light and dark is a token swap, not a redesign. In Figma, design both modes for every screen. In code, use the platform's colour adaptors (UIColor with dark mode variants on iOS, @color resources with night qualifier on Android).

Related Resources

Need a Mobile App Built and Designed?

Our team handles UI/UX design and development end-to-end — iOS, Android, or cross-platform. Fixed price, delivered on time.

Design included24-hour responseFixed pricing