Platform Differences and Conditional UI for iOS and Android

Capítulo 7

Estimated reading time: 13 minutes

+ Exercise

Why Platform Differences Matter in React Native

React Native lets you share most of your code across iOS and Android, but the platforms are not identical. They differ in navigation patterns, typography defaults, gesture behavior, system UI conventions, permission flows, and even how certain native components render. A “one-size-fits-all” UI can feel slightly off on each platform: an Android screen might look too spacious if you copy iOS conventions, while an iOS screen might feel cramped or visually inconsistent if you copy Android defaults.

“Platform differences” in React Native usually means two things: (1) you need to detect which platform the app is running on, and (2) you need to conditionally adjust UI, behavior, or styling so the screen feels native. The goal is not to fork your whole app into two separate codebases, but to apply small, targeted differences where they matter.

Core Tools for Platform-Aware Code

Using Platform to detect iOS vs Android

React Native provides the Platform module to check the current OS and to select values based on the platform. The most common properties are Platform.OS (returns "ios" or "android") and Platform.select (lets you define platform-specific values in one place).

import { Platform } from 'react-native';

const isIOS = Platform.OS === 'ios';

const paddingTop = Platform.select({
  ios: 12,
  android: 8,
  default: 10,
});

Platform.select is especially useful for styles and small behavioral differences because it keeps the branching logic compact.

Using Platform.Version for OS-version differences

Sometimes the difference is not iOS vs Android, but “Android 12+” vs “older Android.” You can use Platform.Version to branch based on the OS version. On Android it’s typically a number (API level). On iOS it can be a string or number depending on React Native version; treat it carefully and prefer feature detection when possible.

Continue in our app.
  • Listen to the audio with the screen off.
  • Earn a certificate upon completion.
  • Over 5000 courses for you to explore!
Or continue reading below...
Download App

Download the app

import { Platform } from 'react-native';

const isAndroid12Plus = Platform.OS === 'android' && Platform.Version >= 31;

Platform-specific files: .ios and .android

React Native’s bundler supports platform extensions. If you create two files with the same base name, React Native will automatically pick the correct one:

  • ButtonPrimary.ios.js
  • ButtonPrimary.android.js

This is useful when the implementation differs significantly (for example, using different native modules or different component structures). Use it sparingly; prefer a single shared component with small conditional differences when possible.

// ButtonPrimary.js usage stays the same:
import ButtonPrimary from './ButtonPrimary';

// React Native resolves to ButtonPrimary.ios.js or ButtonPrimary.android.js automatically.

Conditional Rendering Patterns for UI

Pattern 1: Inline conditional rendering

For small UI differences, inline conditions are fine. Keep them short and readable.

import { Platform, Text } from 'react-native';

export function HeaderTitle() {
  return (
    <Text>
      {Platform.OS === 'ios' ? 'Settings' : 'Settings'}
    </Text>
  );
}

The example above is intentionally trivial; in real code, you might render a different icon, show a different hint, or adjust spacing.

Pattern 2: Extract platform-specific values (recommended)

Instead of sprinkling Platform.OS checks throughout JSX, define platform-specific constants and use them in your component. This keeps rendering logic clean.

import { Platform, StyleSheet, Text, View } from 'react-native';

const COPY = {
  title: 'Account',
  hint: Platform.select({
    ios: 'Use Face ID to sign in faster.',
    android: 'Use biometrics to sign in faster.',
    default: 'Use device security to sign in faster.',
  }),
};

export function AccountHint() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>{COPY.title}</Text>
      <Text style={styles.hint}>{COPY.hint}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    padding: 16,
  },
  title: {
    fontSize: 18,
    fontWeight: Platform.select({ ios: '600', android: '700' }),
  },
  hint: {
    marginTop: 8,
    color: '#555',
  },
});

Pattern 3: Component-level branching

If the UI structure differs (not just values), branch at the component level. Keep each branch as small as possible.

import { Platform, Pressable, Text } from 'react-native';

function IOSAction({ onPress, label }) {
  return (
    <Pressable onPress={onPress} style={{ paddingVertical: 12 }}>
      <Text style={{ color: '#007AFF', fontSize: 17 }}>{label}</Text>
    </Pressable>
  );
}

function AndroidAction({ onPress, label }) {
  return (
    <Pressable
      onPress={onPress}
      android_ripple={{ color: 'rgba(0,0,0,0.12)' }}
      style={{ paddingVertical: 12 }}
    >
      <Text style={{ color: '#1B5E20', fontSize: 16, fontWeight: '600' }}>
        {label}
      </Text>
    </Pressable>
  );
}

export function PlatformAction(props) {
  return Platform.OS === 'ios' ? <IOSAction {...props} /> : <AndroidAction {...props} />;
}

This approach is helpful when you want iOS to look like a typical “tappable text” action while Android uses ripple feedback and slightly different typography.

Platform-Specific Styling Without Duplicating Stylesheets

Using Platform.select inside styles

Many platform differences are purely stylistic: font weights, shadows, elevation, and default spacing. You can keep a single StyleSheet and use Platform.select for the few properties that differ.

import { Platform, StyleSheet } from 'react-native';

const styles = StyleSheet.create({
  card: {
    backgroundColor: 'white',
    borderRadius: 12,
    padding: 16,
    ...Platform.select({
      ios: {
        shadowColor: '#000',
        shadowOpacity: 0.12,
        shadowRadius: 10,
        shadowOffset: { width: 0, height: 6 },
      },
      android: {
        elevation: 4,
      },
    }),
  },
});

On iOS, shadows are controlled by shadow* properties. On Android, shadows are typically controlled by elevation. This is one of the most common platform styling differences you’ll handle.

Font differences and consistent typography

Even if you use the same fontSize, text can appear slightly different across platforms due to font rendering and default font families. If you want a consistent look, define a typography helper that selects font weights or families per platform.

import { Platform } from 'react-native';

export const typography = {
  title: {
    fontSize: 20,
    fontWeight: Platform.select({ ios: '600', android: '700' }),
  },
  body: {
    fontSize: 16,
    lineHeight: Platform.select({ ios: 22, android: 24 }),
  },
};

Note that Android supports a wider range of numeric font weights depending on the font family. iOS often maps weights differently. Testing on real devices is important when typography is central to your design.

Touch Feedback Differences: Ripple vs Opacity

Users expect different touch feedback. Android commonly uses ripple effects; iOS commonly uses opacity changes or highlight states. React Native’s Pressable supports both patterns: use android_ripple on Android, and use a pressed style on iOS.

import { Platform, Pressable, Text } from 'react-native';

export function PrimaryButton({ label, onPress }) {
  return (
    <Pressable
      onPress={onPress}
      android_ripple={Platform.OS === 'android' ? { color: 'rgba(255,255,255,0.25)' } : undefined}
      style={({ pressed }) => [
        {
          backgroundColor: '#2E7D32',
          paddingVertical: 12,
          paddingHorizontal: 16,
          borderRadius: 10,
          opacity: Platform.OS === 'ios' && pressed ? 0.7 : 1,
        },
      ]}
    >
      <Text style={{ color: 'white', fontWeight: '600', textAlign: 'center' }}>
        {label}
      </Text>
    </Pressable>
  );
}

This keeps the component shared while still feeling native on each platform.

Keyboard and Input Differences You’ll Actually Notice

Text input and keyboard behavior can differ across platforms. For example, iOS often overlays the keyboard and expects content to move; Android behavior varies by device and window settings. Even without diving into safe areas (covered earlier), you can still apply platform-aware tweaks for inputs.

Choosing appropriate keyboard types and return key behavior

Some keyboard options behave differently across platforms. Always test your chosen combination.

import { TextInput, Platform } from 'react-native';

export function EmailField({ value, onChangeText }) {
  return (
    <TextInput
      value={value}
      onChangeText={onChangeText}
      autoCapitalize="none"
      autoCorrect={false}
      keyboardType={Platform.OS === 'ios' ? 'email-address' : 'email-address'}
      returnKeyType={Platform.OS === 'ios' ? 'done' : 'next'}
      textContentType={Platform.OS === 'ios' ? 'emailAddress' : undefined}
      placeholder="you@example.com"
      style={{ padding: 12, borderWidth: 1, borderColor: '#DDD', borderRadius: 10 }}
    />
  );
}

On iOS, textContentType can improve autofill behavior. On Android, autofill is handled differently and may not use the same prop in the same way.

Status Bar and System UI: Platform-Aware Choices

iOS and Android have different conventions for status bar text color and background handling. React Native provides a StatusBar component that can be configured per screen or globally. A common approach is to set the bar style based on your theme and set Android background color when needed.

import { Platform, StatusBar } from 'react-native';

export function ScreenStatusBar() {
  return (
    <>
      <StatusBar
        barStyle="dark-content"
        backgroundColor={Platform.OS === 'android' ? '#FFFFFF' : undefined}
      />
    </>
  );
}

On iOS, backgroundColor is not applied the same way as Android; the system manages it differently. This is a typical example of a prop that matters on one platform more than the other.

Alerts, Action Sheets, and Confirmations

Even when you use a cross-platform API like Alert, the UI will look different on iOS vs Android. That’s good: it matches platform conventions. But sometimes you want different button ordering or wording to fit expectations (for example, iOS often places the destructive action separately or emphasizes it differently).

import { Alert, Platform } from 'react-native';

export function confirmDelete({ onConfirm }) {
  const buttons = Platform.select({
    ios: [
      { text: 'Cancel', style: 'cancel' },
      { text: 'Delete', style: 'destructive', onPress: onConfirm },
    ],
    android: [
      { text: 'No' },
      { text: 'Yes, delete', onPress: onConfirm },
    ],
    default: [
      { text: 'Cancel' },
      { text: 'Delete', onPress: onConfirm },
    ],
  });

  Alert.alert('Delete item?', 'This cannot be undone.', buttons);
}

This keeps the same feature while aligning with typical platform phrasing and button styles.

Practical Step-by-Step: Building a Platform-Adaptive “Settings Row”

In many apps, a “settings row” is a reusable component: a label on the left, optional value on the right, and a chevron or navigation indicator. On iOS, rows often have a subtle separator and a right chevron that matches iOS style. On Android, rows often use ripple feedback and slightly different spacing and typography.

Step 1: Define the component API

We’ll create a SettingsRow component with these props:

  • title: left label
  • value: optional right-side text
  • onPress: optional handler (if provided, row is tappable)
// SettingsRow.js
import React from 'react';
import { Platform, Pressable, StyleSheet, Text, View } from 'react-native';

export function SettingsRow({ title, value, onPress }) {
  const Container = onPress ? Pressable : View;

  return (
    <Container
      onPress={onPress}
      android_ripple={onPress && Platform.OS === 'android' ? { color: 'rgba(0,0,0,0.08)' } : undefined}
      style={({ pressed }) => [
        styles.row,
        Platform.OS === 'ios' && onPress && pressed ? styles.rowPressedIOS : null,
      ]}
    >
      <Text style={styles.title}>{title}</Text>

      <View style={styles.right}>
        {value ? <Text style={styles.value}>{value}</Text> : null}
        {onPress ? <Text style={styles.chevron}>›</Text> : null}
      </View>
    </Container>
  );
}

const styles = StyleSheet.create({
  row: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingVertical: Platform.select({ ios: 14, android: 12 }),
    paddingHorizontal: 16,
    backgroundColor: 'white',
    borderBottomWidth: Platform.select({ ios: StyleSheet.hairlineWidth, android: 0 }),
    borderBottomColor: '#E5E5EA',
  },
  rowPressedIOS: {
    backgroundColor: '#F2F2F7',
  },
  title: {
    fontSize: 16,
    color: '#111',
    fontWeight: Platform.select({ ios: '400', android: '500' }),
  },
  right: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 8,
  },
  value: {
    fontSize: 15,
    color: '#666',
  },
  chevron: {
    fontSize: Platform.select({ ios: 22, android: 20 }),
    color: '#C7C7CC',
    marginTop: Platform.select({ ios: -1, android: 0 }),
  },
});

Notice what we did:

  • We used Pressable only when onPress exists, otherwise we render a plain View.
  • We used ripple on Android and a pressed background on iOS.
  • We used a hairline separator on iOS, which is a common iOS list style.
  • We used small typography differences to better match each platform.

Step 2: Use the component in a settings screen

import React from 'react';
import { View } from 'react-native';
import { SettingsRow } from './SettingsRow';

export function SettingsScreen() {
  return (
    <View style={{ backgroundColor: '#F2F2F7', flex: 1 }}>
      <View style={{ marginTop: 12 }}>
        <SettingsRow title="Account" value="Personal" onPress={() => {}} />
        <SettingsRow title="Notifications" onPress={() => {}} />
        <SettingsRow title="Privacy" onPress={() => {}} />
      </View>

      <View style={{ marginTop: 24 }}>
        <SettingsRow title="App Version" value="1.0.0" />
      </View>
    </View>
  );
}

This demonstrates a realistic pattern: tappable rows show a chevron; non-tappable rows (like “App Version”) do not.

Step 3: Decide when to use platform-specific files instead

If your settings rows need completely different visuals (for example, iOS grouped table sections with inset rounded corners vs Android Material list items with leading icons), you might split into SettingsRow.ios.js and SettingsRow.android.js. A good rule of thumb is: if more than ~30–40% of the component differs, consider platform files; otherwise keep one component with small conditional differences.

Practical Step-by-Step: Platform-Specific Navigation Header Behavior (UI Only)

Even if you use the same navigation library across platforms, you may want different header UI details: back button text visibility, title alignment, and action placement. The exact API depends on your navigation solution, but the platform-aware idea is the same: define options using Platform.select and keep the rest shared.

The example below shows a generic pattern of building a platform-aware options object you can pass to your navigator or screen configuration.

import { Platform } from 'react-native';

export const screenHeaderOptions = {
  titleAlign: Platform.select({ ios: 'center', android: 'left' }),
  backTitleVisible: Platform.select({ ios: false, android: false }),
  headerHeight: Platform.select({ ios: 44, android: 56 }),
};

Even if the exact option names differ in your setup, the approach remains: centralize platform differences into a small configuration object so screens stay clean.

Common UI Differences to Watch For

Scrolling behavior and overscroll

iOS often shows “bounce” overscroll; Android often shows a glow effect (or no visible effect depending on version and settings). If your design relies on a specific overscroll behavior, test on both platforms. Prefer designs that don’t depend on overscroll visuals.

Switches, sliders, and pickers

Controls like toggles and pickers can look and behave differently across platforms. If you use the platform’s native control, the look will differ—and that’s usually desirable. If you need a uniform look, you may need a custom component, but that can reduce platform familiarity. A practical compromise is to keep native controls but adjust surrounding layout and labels per platform.

Shadows and elevation

As shown earlier, iOS uses shadow properties while Android uses elevation. When you design cards, floating buttons, or modals, ensure you implement both. Also note that elevation affects the drawing order on Android in ways that can surprise you (e.g., overlapping views).

Organizing Platform Logic So It Doesn’t Get Messy

Create a small “platform tokens” module

Instead of repeating Platform.select across many files, create a single module that exports platform-aware tokens for spacing, typography, and component defaults. This keeps your UI consistent and makes it easy to adjust later.

// uiTokens.js
import { Platform } from 'react-native';

export const uiTokens = {
  headerHeight: Platform.select({ ios: 44, android: 56 }),
  rowPaddingV: Platform.select({ ios: 14, android: 12 }),
  titleWeight: Platform.select({ ios: '600', android: '700' }),
  cardShadow: Platform.select({
    ios: {
      shadowColor: '#000',
      shadowOpacity: 0.12,
      shadowRadius: 10,
      shadowOffset: { width: 0, height: 6 },
    },
    android: {
      elevation: 4,
    },
  }),
};

Then use it in components:

import { StyleSheet } from 'react-native';
import { uiTokens } from './uiTokens';

const styles = StyleSheet.create({
  header: {
    height: uiTokens.headerHeight,
  },
  card: {
    backgroundColor: 'white',
    borderRadius: 12,
    padding: 16,
    ...uiTokens.cardShadow,
  },
});

Prefer feature-based decisions when possible

Sometimes you’re tempted to branch on platform when the real issue is feature support. For example, you might want a blur effect, a haptic feedback pattern, or a specific animation. If your decision is really “is this feature available,” prefer checking whether the feature exists (or whether a library is installed) rather than assuming based on OS. Platform checks are still useful, but feature checks can make your code more robust.

Debugging Platform Differences Efficiently

Make differences visible during development

When you add conditional UI, it’s easy to forget to test both branches. A simple technique is to temporarily render the current platform in a debug-only badge or log platform-specific values when the screen mounts. Keep this out of production builds, but use it while developing to confirm your conditions are doing what you expect.

import { Platform } from 'react-native';

console.log('Running on:', Platform.OS, 'version:', Platform.Version);

Test on real devices for touch and typography

Emulators and simulators are great for layout, but touch feedback, font rendering, and performance can differ on real hardware. Platform-specific UI work is exactly where device testing pays off.

Now answer the exercise about the content:

When you need small platform-specific styling differences in a shared React Native component, which approach best keeps the code clean and avoids scattered OS checks?

You are right! Congratulations, now go to the next page

You missed! Try again.

Platform.select centralizes differences (like spacing, fontWeight, or shadows vs elevation) so components stay shared and JSX remains readable, while still matching platform conventions.

Next chapter

Navigation Concepts: Screens, Routes, and Common App Flows

Arrow Right Icon
Free Ebook cover React Native Basics: Components, Styling, and Navigation Concepts
58%

React Native Basics: Components, Styling, and Navigation Concepts

New course

12 pages

Download the app to earn free Certification and listen to the courses in the background, even with the screen off.