Platform-Specific Code
Platform.OS, Platform.select, and .ios/.android file extensions.
Search across all documentation pages
Platform.OS, Platform.select, and .ios/.android file extensions.
Quick-reference recipe card - copy-paste ready.
import { Platform, StyleSheet, Text, View } from "react-native";
const HEADER_HEIGHT = Platform.select({ ios: 44, android: 56, default: 48 });
export function ScreenHeader({ title }: { title: string }) {
return (
<View style={styles.header}>
<Text style={styles.title}>{title}</Text>
</View>
);
}
const styles = StyleSheet.create({
header: {
height: HEADER_HEIGHT,
paddingTop: Platform.OS === "ios" ? 4 : 0,
backgroundColor: Platform.select({ ios: "#f8f8f8", android: "#6200ee" }),
justifyContent: "center",
paddingHorizontal: 16,
},
title: {
fontSize: 17,
fontWeight: Platform.select({ ios: "600", android: "500" }),
color: Platform.OS === "android" ? "#fff" : "#000",
},
});When to reach for this: Small styling or constant differences between iOS and Android in a single shared component file.
// AppButton.tsx - shared entry; Metro resolves platform files automatically
import { AppButton } from "./components/AppButton";
export default function CheckoutScreen() {
return (
<AppButton
label="Pay now"
onPress={() => console.log("checkout")}
/>
);
}// components/AppButton.tsx - shared API, re-exports platform implementation
export { AppButton } from "./AppButton.native";
export type { AppButtonProps } from "./AppButton.types";// components/AppButton.types.ts
export interface AppButtonProps {
label: string;
onPress: () => void;
disabled?: boolean;
}// components/AppButton.native.tsx - fallback for non-split platforms
import { Platform } from "react-native";
import { AppButtonIos } from "./AppButton.ios";
import { AppButtonAndroid } from "./AppButton.android";
export function AppButton(props: import("./AppButton.types").AppButtonProps) {
return Platform.OS === "android" ? (
<AppButtonAndroid {...props} />
) : (
<AppButtonIos {...props} />
);
}// components/AppButton.ios.tsx
import { Pressable, StyleSheet, Text } from "react-native";
import type { AppButtonProps } from "./AppButton.types";
export function AppButtonIos({ label, onPress, disabled }: AppButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
style={({ pressed }) => [
styles.button,
pressed && styles.pressed,
disabled && styles.disabled,
]}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: "#007aff",
borderRadius: 10,
minHeight: 44,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 20,
},
pressed: { opacity: 0.7 },
disabled: { opacity: 0.4 },
label: { color: "#fff", fontSize: 17, fontWeight: "600" },
});// components/AppButton.android.tsx
import { Pressable, StyleSheet, Text } from "react-native";
import type { AppButtonProps } from "./AppButton.types";
export function AppButtonAndroid({ label, onPress, disabled }: AppButtonProps) {
return (
<Pressable
onPress={onPress}
disabled={disabled}
android_ripple={{ color: "rgba(255,255,255,0.3)" }}
style={[styles.button, disabled && styles.disabled]}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: "#6750a4",
borderRadius: 4,
minHeight: 48,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 24,
elevation: 2,
},
disabled: { opacity: 0.4 },
label: { color: "#fff", fontSize: 15, fontWeight: "500", textTransform: "uppercase" },
});// utils/share.ts - shared module with Platform.select for behavior
import { Platform, Share } from "react-native";
export async function shareUrl(url: string, message: string) {
if (Platform.OS === "web") {
if (navigator.share) {
await navigator.share({ title: message, url });
return;
}
await navigator.clipboard.writeText(url);
return;
}
await Share.share(
Platform.select({
ios: { url, message },
android: { message: `${message}\n${url}` },
default: { message: `${message} ${url}` },
})!
);
}What this demonstrates:
Platform.select for constants and Share.share payload differences in one file.ios.tsx / .android.tsx files for materially different button chromeAppButton.types.ts keeping a single public props contract.ios.js / .android.js / .native.js / .jsPlatform.OS === "web" branch for Expo web targets without a separate web file.ios.tsx → .native.tsx → .tsx (on iOS sim/device).Platform.OS is a runtime string read on the JS thread - safe inside render and handlers.Platform.select returns the matching platform key or default - can return objects, numbers, or functions..ios.tsx module.Platform.Version with feature detection.| Import | iOS bundle loads | Android bundle loads |
|---|---|---|
./Foo | Foo.ios.tsx → Foo.native.tsx → Foo.tsx | Foo.android.tsx → Foo.native.tsx → Foo.tsx |
./Foo.ios | Explicit iOS file only | N/A unless imported |
.native.tsx runs on iOS and Android but not web when a .web.tsx sibling exists..web.tsx is chosen for Expo web exports.// Object form - pick a value
const font = Platform.select({ ios: "System", android: "Roboto", default: "sans-serif" });
// Function form - lazy per platform
const createStyles = Platform.select({
ios: () => StyleSheet.create({ card: { shadowOpacity: 0.2 } }),
android: () => StyleSheet.create({ card: { elevation: 4 } }),
});
// Inline ternary - fine for one-off props
paddingTop: Platform.OS === "ios" ? 8 : 0,| Approach | Best for | Avoid when |
|---|---|---|
Platform.select in StyleSheet | Colors, heights, font weights | Entire components differ |
Platform.OS if/else | Conditional logic, early returns | Many duplicated JSX blocks |
.ios / .android files | Large UI or native module wrappers | One-line color tweak |
*.web.tsx | Expo web-specific DOM APIs | Mobile-only apps with no web target |
import { Platform } from "react-native";
const iosVersion =
Platform.OS === "ios" ? parseInt(String(Platform.Version), 10) : 0;
const androidApi =
Platform.OS === "android" ? Platform.Version : 0;
const supportsBlur = iosVersion >= 13 || androidApi >= 31;Platform.Version on iOS is a string like "17.0" - parse before numeric compare.34).import { Platform, type PlatformOSType } from "react-native";
type MobileOS = Extract<PlatformOSType, "ios" | "android">;
function isMobile(os: PlatformOSType): os is MobileOS {
return os === "ios" || os === "android";
}
// Platform.select return may be undefined - provide default
const height = Platform.select({ ios: 44, android: 56 }) ?? 48;
// Share platform-specific modules with identical exports
export type { AppButtonProps } from "./AppButton.types";PlatformOSType includes "web" in Expo - narrow before using mobile-only APIs.Platform.select without default can return undefined - use ?? fallback..types.ts file imported by all platform variants.Splitting files too early - Three one-line color differences do not need .ios and .android files. Fix: Use Platform.select until JSX diverges materially.
Importing .ios files directly - import X from "./Foo.ios" breaks Android bundles or ships the wrong module. Fix: Import ./Foo and let Metro resolve.
Forgetting default in Platform.select - Web or macos targets get undefined. Fix: Always add default for Expo multi-platform apps.
Platform checks in shared styles outside create - Calling Platform.select once at module scope is fine; dynamic per-theme values belong inside hooks.
Duplicated business logic across platform files - Copy-paste drifts over time. Fix: Share logic in utils/*.ts; split only the view/native boundary.
Assuming Android === Material, iOS === Human Interface - Users expect brand consistency. Fix: Unify spacing and semantics; vary chrome only where OS conventions demand it.
Comparing Platform.Version as string on iOS - "17" < "9" lexicographically fails. Fix: parseInt(String(Platform.Version), 10) before numeric compare.
| Alternative | Use When | Don't Use When |
|---|---|---|
Platform.select | Small style/constant deltas | Whole components differ |
.ios / .android files | Large UI or native imports per OS | Single color or height tweak |
expo-device / Constants | Device model, isTablet, runtimeVersion | Simple OS enum suffices |
| Conditional native modules | Feature requires TurboModule only on one OS | JS-only styling difference |
| Universal design | Brand mandates pixel parity | OS HIG materially affects UX (share sheets) |
react-native-unistyles breakpoints | Theme tokens per platform in design system | One-off header height |
"ios" and "android"."web", and RN supports "macos" and "windows" in some templates.Platform.OS === "ios" rather than assuming only two platforms.Foo.ios.tsx first, then Foo.native.tsx, then Foo.tsx.Foo.android.tsx replaces the .ios step.Platform.select for style values, numeric constants, and small config objects.*.web.tsx implementation..ios) beats .native beats generic .tsx.default key, unmatched platforms receive undefined.default or an explicit web key.Platform.select({...}) ?? fallback.app/ but components can split normally..ios unless you intend OS-specific screens.const spacing = Platform.select({
ios: 8,
android: 12,
default: 10,
}); // number | undefined without default - use default key"18.0") - parse to integer major version for comparisons.34) - compare numerically.{Platform.OS === "ios" && <BlurView />}) are fine.*.web.tsx when present..web, web may fall back to .tsx or .native.tsx depending on resolver config.*.web.tsx when mobile code imports native-only modules../Button always - let the bundler pick the right file.Component.types.ts with props interfaces.Component.ios.tsx / Component.android.tsx imports the same types.Component.tsx barrel that re-exports the resolved implementation.Platform.OS via jest.spyOn(Platform, "OS", "get").Platform.OS is "web".ios - set explicitly per suite.expo-constants exposes executionEnvironment, sessionId, platform subtleties, and EAS metadata.Platform for quick OS branches in UI components..ios.ts / .android.ts stubs with matching TypeScript signatures.index.ts so app code stays platform-agnostic.Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026