Stop Hardcoding Colors: PlatformColor Makes React Native Apps Feel Native
How PlatformColor solves dark mode nightmares and makes your React Native app feel truly native with iOS and Android semantic colors.
I discovered PlatformColor while rebuilding the Pinx app, and honestly, I wish I'd known about it years ago. Here's the problem: you hardcode #FF0000 for a nice red accent color, ship your app, and then someone turns on dark mode and your beautiful red is either invisible against a dark background or blindingly aggressive.
Worse, your app looks foreign on both platforms. iOS users expect that soft, adaptive systemRed. Android users expect Material Design theme colors. Instead, they're getting your hardcoded hex values that don't respect their system preferences.
PlatformColor fixes this. It's a React Native API that lets you tap directly into native platform colors—the same semantic colors that Apple and Google use in their own apps. And they automatically adapt to light mode, dark mode, high contrast, and whatever else the user throws at you.
What PlatformColor Actually Does
PlatformColor is deceptively simple. Instead of this:
<Text style={{ color: '#FF0000' }}>Error message</Text>You write this:
import { Platform, PlatformColor } from 'react-native';
<Text style={{
color: PlatformColor(
Platform.OS === 'android' ? '@android:color/holo_red_dark' : 'systemRedColor'
)
}}>
Error message
</Text>Now your text uses iOS's systemRedColor on iPhone and Android's holo_red_dark on Android. Both automatically adapt to the user's theme settings. When they switch to dark mode, the colors adjust—lighter on dark backgrounds, darker on light backgrounds—without you writing a single line of theme-switching logic.
Real Implementation: Native Tab Bars
Here's actual code from the Pinx rebuild where we used PlatformColor for native tab bar icons:
import { Platform, PlatformColor } from 'react-native';
<NativeTabs
iconColor={PlatformColor(
Platform.OS === 'android'
? '@android:color/holo_red_dark'
: 'systemRedColor'
)}
tintColor={PlatformColor(
Platform.OS === 'android'
? '@android:color/holo_red_dark'
: 'systemRedColor'
)}
>
{/* tabs content */}
</NativeTabs>This gives us a consistent red accent across platforms, but each platform gets its red—not some compromise middle ground. iOS users see systemRedColor (which is actually #FF3B30 in light mode but adjusts in dark mode). Android users see holo_red_dark (#CC0000). Both feel native because they are native.
The Comprehensive Color Reference
This is the table I wish existed when I started. It maps iOS semantic colors to their closest Android equivalents and explains what each is actually for.
System Colors (Brand/Accent Colors)
| iOS Color | Android Equivalent | Purpose |
|---|---|---|
systemRed | @android:color/holo_red_dark or ?attr/colorError | Destructive actions, errors |
systemBlue | ?attr/colorPrimary | Primary actions, links, selection |
systemGreen | @android:color/holo_green_dark | Success states, confirmation |
systemOrange | @android:color/holo_orange_dark | Warnings, secondary alerts |
systemYellow | @android:color/holo_orange_light | Caution, highlights |
systemPink | ?attr/colorSecondary | Accent, creative actions |
systemPurple | @android:color/holo_purple | Creative content, special features |
systemTeal | @android:color/holo_blue_dark | Information, secondary branding |
systemIndigo | ?attr/colorPrimaryVariant | Deep actions, premium features |
Gray Scale (Neutral Tones)
| iOS Color | Android Equivalent | Purpose |
|---|---|---|
systemGray | @android:color/darker_gray | Disabled state, subtle elements |
systemGray2 | ?attr/colorControlNormal | Secondary UI elements |
systemGray3 | ?attr/colorControlHighlight | Subtle separators |
systemGray4 | @android:color/lighter_gray | Background tints |
systemGray5 | ?attr/colorSurface | Card backgrounds |
systemGray6 | ?attr/colorBackground | Deepest gray before background |
Text/Label Colors
| iOS Color | Android Equivalent | Purpose |
|---|---|---|
label | ?attr/colorOnBackground | Primary text, body copy |
secondaryLabel | ?attr/colorOnSurface (60% opacity) | Subheadings, secondary info |
tertiaryLabel | ?attr/colorOnSurface (38% opacity) | Tertiary text, placeholders |
quaternaryLabel | ?attr/colorOnSurface (12% opacity) | Disabled text, watermarks |
placeholderText | ?android:attr/textColorHint | Input placeholders |
Background Colors (Standard Stack)
| iOS Color | Android Equivalent | Purpose |
|---|---|---|
systemBackground | ?attr/colorBackground | Primary background (white/black) |
secondarySystemBackground | ?attr/colorSurface | Cards, modals, raised content |
tertiarySystemBackground | ?attr/colorPrimarySurface | Grouped content backgrounds |
Background Colors (Grouped Stack)
| iOS Color | Android Equivalent | Purpose |
|---|---|---|
systemGroupedBackground | ?attr/colorBackground | Table grouped style background |
secondarySystemGroupedBackground | ?attr/colorSurface | Grouped content cells |
tertiarySystemGroupedBackground | ?attr/colorPrimarySurface | Nested grouped content |
Fill Colors (Semi-Transparent Overlays)
| iOS Color | Android Equivalent | Purpose |
|---|---|---|
systemFill | ?attr/colorControlHighlight | Button backgrounds (filled style) |
secondarySystemFill | ?attr/colorControlActivated (lower opacity) | Secondary button fills |
tertiarySystemFill | ?attr/colorSurface (subtle tint) | Tertiary fills |
quaternarySystemFill | ?attr/colorSurface (very subtle) | Barely-there fills |
UI Elements
| iOS Color | Android Equivalent | Purpose |
|---|---|---|
separator | ?android:attr/dividerVertical | Dividers, hairlines (translucent) |
opaqueSeparator | ?android:attr/listDivider | Solid dividers (non-transparent) |
link | ?attr/colorPrimary | Hyperlinks, tappable text |
Android-Specific Theme Attributes
These don't have direct iOS equivalents but are crucial for Android:
| Android Attribute | Purpose |
|---|---|
?attr/colorPrimary | Primary brand color |
?attr/colorPrimaryVariant | Darker/lighter primary variant |
?attr/colorSecondary | Secondary brand accent |
?attr/colorAccent | Legacy accent (pre-Material 3) |
?attr/colorControlActivated | Active checkboxes, switches |
?attr/colorControlNormal | Inactive controls |
Platform Differences: Android Prefixes Matter
Android has two color systems, and the prefix matters:
@android:color/ - System colors (fixed values)
PlatformColor('@android:color/holo_red_dark') // Always #CC0000?attr/ - Theme attributes (app-controlled)
PlatformColor('?attr/colorPrimary') // Whatever your app theme definesUse ?attr/ for colors that should match your app's branding. Use @android:color/ when you want a specific Android system color.
iOS doesn't have this distinction—all colors are semantic and adaptive.
Practical Pattern: Platform.select()
For cleaner code, use Platform.select():
const styles = StyleSheet.create({
errorText: {
color: Platform.select({
ios: PlatformColor('systemRedColor'),
android: PlatformColor('?attr/colorError'),
default: '#FF0000', // Web fallback
}),
},
primaryButton: {
backgroundColor: Platform.select({
ios: PlatformColor('systemBlue'),
android: PlatformColor('?attr/colorPrimary'),
default: '#007AFF',
}),
},
bodyText: {
color: Platform.select({
ios: PlatformColor('label'),
android: PlatformColor('?attr/colorOnBackground'),
default: '#000000',
}),
},
});This reads better than ternaries everywhere and handles web fallbacks gracefully.
Gotchas from Real Usage
1. Styled Components and NativeWind don't support PlatformColor
If you're using a CSS-in-JS library, PlatformColor might not work. We hit this in Pinx—had to use standard StyleSheet for components that needed PlatformColor.
2. Test in BOTH light and dark mode
Obvious but easy to forget. What looks great in light mode might be invisible in dark mode. Use iOS simulator appearance toggle and Android emulator theme settings constantly.
3. Fallbacks are critical
Always provide a fallback color:
color: Platform.OS === 'web'
? '#FF0000'
: PlatformColor('systemRedColor')React Native Web doesn't support PlatformColor, so you need explicit fallbacks or your web build breaks.
4. Not all libraries pass PlatformColor correctly
Some third-party components expect strings or hex values. If you pass PlatformColor and see errors, that's probably why. You might need to fork the component or submit a PR.
DynamicColorIOS: Advanced iOS Control
For more granular control on iOS, there's DynamicColorIOS:
import { DynamicColorIOS } from 'react-native';
const dynamicRed = DynamicColorIOS({
light: '#FF3B30',
dark: '#FF453A',
highContrastLight: '#D70015',
highContrastDark: '#FF6961',
});This lets you specify exact values for each appearance mode. We haven't needed it yet—semantic colors handle 95% of cases—but it's there if you need pixel-perfect control.
When to Use PlatformColor
✅ USE IT FOR:
- UI chrome (tab bars, nav bars, toolbars)
- Text colors (body, headings, secondary)
- Backgrounds (screens, cards, modals)
- System UI elements (separators, fills)
- Destructive/success/warning states
❌ DON'T USE IT FOR:
- Brand colors that MUST be exact (your logo red)
- Custom illustrations or graphics
- Colors that are part of your visual identity
- When supporting web (no PlatformColor support)
Why This Matters
Using PlatformColor isn't just about dark mode (though that's huge). It's about respecting user preferences. High contrast mode. Increased contrast. Color blindness accommodations. All of those work because you're using semantic colors the OS knows how to adjust.
Plus, your app feels native. Users might not consciously notice, but when your reds match Safari's reds and your backgrounds match Settings' backgrounds, the app feels like it belongs on their device.
Best Practices We've Learned
- Start with PlatformColor from day one - Retrofitting is painful
- Use the table above as a reference - Keep it handy when styling
- Create a colors constant file - Centralize your PlatformColor definitions
- Test appearance changes live - Toggle dark mode while the app is running
- Fallback to web-safe colors - Always have a plan for non-native platforms
- Document your color choices - Future you will forget why you chose
systemGray3oversystemGray4
Our Color Constants Pattern
Here's how we organize PlatformColors in Pinx:
// colors.ts
import { Platform, PlatformColor } from 'react-native';
export const Colors = {
text: {
primary: Platform.select({
ios: PlatformColor('label'),
android: PlatformColor('?attr/colorOnBackground'),
default: '#000000',
}),
secondary: Platform.select({
ios: PlatformColor('secondaryLabel'),
android: PlatformColor('?attr/colorOnSurface'),
default: '#666666',
}),
},
background: {
primary: Platform.select({
ios: PlatformColor('systemBackground'),
android: PlatformColor('?attr/colorBackground'),
default: '#FFFFFF',
}),
secondary: Platform.select({
ios: PlatformColor('secondarySystemBackground'),
android: PlatformColor('?attr/colorSurface'),
default: '#F2F2F7',
}),
},
accent: {
primary: Platform.select({
ios: PlatformColor('systemBlue'),
android: PlatformColor('?attr/colorPrimary'),
default: '#007AFF',
}),
destructive: Platform.select({
ios: PlatformColor('systemRed'),
android: PlatformColor('?attr/colorError'),
default: '#FF3B30',
}),
},
};Then import and use: color: Colors.text.primary. Clean, consistent, and easy to update.
The Bottom Line
PlatformColor turned dark mode from a nightmare into a non-issue. We don't think about it anymore. Define your colors once with semantic platform colors, and they just work across appearances, platforms, and accessibility settings.
The comprehensive table above should save you hours of digging through Apple and Android docs. Bookmark it, reference it, and your apps will feel more native with way less effort.
Building a React Native app that needs to feel truly native? We've now shipped several apps using PlatformColor and learned all the edge cases. If you'd like help building something that respects platform conventions while maintaining your brand, get in touch.