Skip to main content

React Native UI Components

Component architecture, styling patterns, theme management, and responsive design.

React Native's styling system is inspired by web CSS but operates differently under the hood. Instead of cascading stylesheets, React Native uses JavaScript objects that define styles, which are then translated to platform-specific view attributes. A backgroundColor: '#FFFFFF' style becomes setBackgroundColor(Color.WHITE) on Android and backgroundColor = UIColor.white on iOS. This translation happens at runtime, ensuring platform-native rendering.

StyleSheet.create() is essential for performance. While you could use inline style objects, StyleSheet.create() validates styles at creation time and optimizes them for the bridge communication layer. For banking apps with complex UIs (transaction lists, payment forms, account dashboards), this optimization prevents unnecessary JavaScript-to-native traffic on every render. The styles are created once and referenced by ID, reducing data transfer across the bridge.

The atomic design pattern (atoms → molecules → organisms → templates → pages) is particularly valuable for banking UIs because it promotes reusability and consistency. Your "Button" atom is used everywhere (login, payment confirm, transfer funds), ensuring consistent touch target sizes, colors, and accessibility labels. When regulatory requirements demand minimum 48dp touch targets, you update one component, not 47 buttons across the app. See our React Guidelines for more on component composition patterns that apply equally to React Native.


Component Architecture


Styling with StyleSheet

// components/PaymentCard.tsx
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform } from 'react-native';
import { formatCurrency, formatDate } from '../utils/formatters';
import type { Payment } from '../types/payment';

interface PaymentCardProps {
payment: Payment;
onPress: () => void;
}

export default function PaymentCard({ payment, onPress }: PaymentCardProps) {
return (
<TouchableOpacity
style={styles.container}
onPress={onPress}
accessibilityLabel={`Payment to ${payment.recipientName} for ${formatCurrency(payment.amount)}`}
accessibilityRole="button"
>
<View style={styles.header}>
<Text style={styles.recipient}>{payment.recipientName}</Text>
<View style={[styles.statusBadge, styles[`status${payment.status}`]]}>
<Text style={styles.statusText}>{payment.status}</Text>
</View>
</View>
<Text style={styles.amount}>{formatCurrency(payment.amount)}</Text>
<Text style={styles.date}>{formatDate(payment.createdAt)}</Text>
</TouchableOpacity>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
padding: 16,
marginHorizontal: 16,
marginVertical: 8,
borderRadius: 12,
...Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 3,
},
}),
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
recipient: {
fontSize: 18,
fontWeight: '600',
color: '#000000',
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
},
statusText: {
fontSize: 12,
fontWeight: '500',
},
statusCompleted: {
backgroundColor: '#E8F5E9',
color: '#2E7D32',
},
statusPending: {
backgroundColor: '#FFF3E0',
color: '#F57C00',
},
statusFailed: {
backgroundColor: '#FFEBEE',
color: '#C62828',
},
amount: {
fontSize: 24,
fontWeight: '700',
color: '#000000',
marginBottom: 4,
},
date: {
fontSize: 14,
color: '#666666',
},
});

Theme Management

// config/theme.ts
export const theme = {
colors: {
primary: '#007AFF',
secondary: '#5856D6',
success: '#34C759',
warning: '#FF9500',
error: '#FF3B30',
background: '#F2F2F7',
surface: '#FFFFFF',
text: '#000000',
textSecondary: '#666666',
border: '#C6C6C8',
},
spacing: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
},
typography: {
h1: {
fontSize: 32,
fontWeight: '700' as const,
},
h2: {
fontSize: 24,
fontWeight: '600' as const,
},
body: {
fontSize: 16,
fontWeight: '400' as const,
},
caption: {
fontSize: 12,
fontWeight: '400' as const,
},
},
borderRadius: {
sm: 4,
md: 8,
lg: 12,
xl: 16,
},
} as const;

// Usage in components
import { theme } from '../config/theme';

const styles = StyleSheet.create({
button: {
backgroundColor: theme.colors.primary,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.md,
},
title: {
...theme.typography.h1,
color: theme.colors.text,
},
});

Responsive Design

Responsive design in React Native requires handling three variables: screen physical size (iPhone SE vs iPad), screen density (pixel ratio), and user accessibility settings (large text). Unlike web development where you might use media queries and relative units, React Native requires programmatic scaling since CSS media queries don't exist.

The Dimensions API provides current screen dimensions in density-independent pixels (dp on Android, points on iOS). These units automatically account for screen density - a 50dp element renders as 50 pixels on a 1x screen, 100 pixels on a 2x screen, and 150 pixels on a 3x screen. However, screen size still varies: iPhone SE (375pt wide) vs iPhone Pro Max (428pt wide) vs iPad (1024pt wide).

The normalize() function scales font sizes proportionally based on a base screen width. Without normalization, 16pt text might look perfect on iPhone 12 but tiny on iPad. This approach scales all font sizes, ensuring readable text across devices. However, be cautious: overly aggressive scaling can make text too large on tablets. Consider using breakpoints (device width thresholds) for tablets rather than pure linear scaling.

PixelRatio.roundToNearestPixel() ensures values align with actual pixels, preventing subpixel rendering artifacts. On a 3x device, only values divisible by 1/3 (0.33, 0.67, 1.0) align perfectly to physical pixels. Rounding prevents antialiasing blur on borders and text.

// utils/responsive.ts
import { Dimensions, PixelRatio } from 'react-native';

const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');

const BASE_WIDTH = 375; // iPhone SE width as baseline
const BASE_HEIGHT = 667;

/**
* Normalize font size for different screen densities
* Scales proportionally based on screen width
*/
export function normalize(size: number): number {
const scale = SCREEN_WIDTH / BASE_WIDTH;
const newSize = size * scale;
return Math.round(PixelRatio.roundToNearestPixel(newSize));
}

/**
* Get percentage of screen width
*/
export function wp(percentage: number): number {
return (SCREEN_WIDTH * percentage) / 100;
}

/**
* Get percentage of screen height
*/
export function hp(percentage: number): number {
return (SCREEN_HEIGHT * percentage) / 100;
}

/**
* Check if device is small (iPhone SE, etc.)
*/
export const isSmallDevice = SCREEN_WIDTH < 375;

/**
* Check if device is tablet
*/
export const isTablet = SCREEN_WIDTH >= 768;

// Usage
import { normalize, wp, hp } from '../utils/responsive';

const styles = StyleSheet.create({
title: {
fontSize: normalize(18),
},
container: {
width: wp(90), // 90% of screen width
height: hp(30), // 30% of screen height
},
});

Reusable Components

Button Component

// components/common/Button.tsx
import React from 'react';
import {
TouchableOpacity,
Text,
StyleSheet,
ActivityIndicator,
ViewStyle,
TextStyle,
} from 'react-native';
import { theme } from '../../config/theme';

interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline';
disabled?: boolean;
loading?: boolean;
style?: ViewStyle;
textStyle?: TextStyle;
}

export default function Button({
title,
onPress,
variant = 'primary',
disabled = false,
loading = false,
style,
textStyle,
}: ButtonProps) {
return (
<TouchableOpacity
style={[
styles.button,
styles[variant],
disabled && styles.disabled,
style,
]}
onPress={onPress}
disabled={disabled || loading}
accessibilityRole="button"
accessibilityState={{ disabled: disabled || loading }}
>
{loading ? (
<ActivityIndicator color="#fff" />
) : (
<Text style={[styles.text, styles[`${variant}Text`], textStyle]}>
{title}
</Text>
)}
</TouchableOpacity>
);
}

const styles = StyleSheet.create({
button: {
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
borderRadius: theme.borderRadius.md,
alignItems: 'center',
justifyContent: 'center',
minHeight: 48, // Accessibility: minimum touch target
},
primary: {
backgroundColor: theme.colors.primary,
},
secondary: {
backgroundColor: theme.colors.secondary,
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.primary,
},
disabled: {
opacity: 0.5,
},
text: {
fontSize: 16,
fontWeight: '600',
},
primaryText: {
color: '#FFFFFF',
},
secondaryText: {
color: '#FFFFFF',
},
outlineText: {
color: theme.colors.primary,
},
});

Form Components

// components/common/Input.tsx
import React, { useState } from 'react';
import { View, TextInput, Text, StyleSheet, TextInputProps } from 'react-native';
import { theme } from '../../config/theme';

interface InputProps extends TextInputProps {
label: string;
error?: string;
}

export default function Input({ label, error, ...props }: InputProps) {
const [isFocused, setIsFocused] = useState(false);

return (
<View style={styles.container}>
<Text style={styles.label}>{label}</Text>
<TextInput
style={[
styles.input,
isFocused && styles.inputFocused,
error && styles.inputError,
]}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholderTextColor={theme.colors.textSecondary}
{...props}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
}

const styles = StyleSheet.create({
container: {
marginBottom: theme.spacing.md,
},
label: {
fontSize: 14,
fontWeight: '500',
color: theme.colors.text,
marginBottom: theme.spacing.xs,
},
input: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.md,
padding: theme.spacing.md,
fontSize: 16,
color: theme.colors.text,
backgroundColor: '#FFFFFF',
},
inputFocused: {
borderColor: theme.colors.primary,
},
inputError: {
borderColor: theme.colors.error,
},
errorText: {
fontSize: 12,
color: theme.colors.error,
marginTop: theme.spacing.xs,
},
});

List Patterns

FlatList with Virtualization

FlatList provides virtualized rendering for long lists, only rendering visible items for optimal performance.

// screens/PaymentListScreen.tsx
import React from 'react';
import { FlatList, RefreshControl } from 'react-native';
import PaymentCard from '../components/PaymentCard';
import type { Payment } from '../types/payment';

interface PaymentListScreenProps {
payments: Payment[];
loading: boolean;
onRefresh: () => void;
onLoadMore: () => void;
onPaymentPress: (paymentId: string) => void;
}

export default function PaymentListScreen({
payments,
loading,
onRefresh,
onLoadMore,
onPaymentPress,
}: PaymentListScreenProps) {
return (
<FlatList
data={payments}
renderItem={({ item }) => (
<PaymentCard
payment={item}
onPress={() => onPaymentPress(item.id)}
/>
)}
keyExtractor={item => item.id}
onEndReached={onLoadMore}
onEndReachedThreshold={0.5}
refreshControl={
<RefreshControl
refreshing={loading}
onRefresh={onRefresh}
/>
}
/>
);
}

Why FlatList: Renders only visible items, recycles components as you scroll. Essential for lists with more than 20-30 items to prevent memory issues and maintain 60 FPS scrolling.

Pull-to-Refresh Pattern

Pull-to-refresh is a standard mobile pattern for refreshing content.

import React, { useState } from 'react';
import { ScrollView, RefreshControl } from 'react-native';

export default function TransactionScreen() {
const [refreshing, setRefreshing] = useState(false);
const [transactions, setTransactions] = useState([]);

const handleRefresh = async () => {
setRefreshing(true);
try {
const freshData = await fetchTransactions();
setTransactions(freshData);
} finally {
setRefreshing(false);
}
};

return (
<ScrollView
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
/>
}
>
{/* Content */}
</ScrollView>
);
}

Gesture Handling

React Native provides basic touch handling, but for advanced gestures use react-native-gesture-handler.

Basic Touch Handling

import React from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';

export default function SimpleButton({ onPress, title }: Props) {
return (
<TouchableOpacity
style={styles.button}
onPress={onPress}
activeOpacity={0.7}
>
<Text style={styles.text}>{title}</Text>
</TouchableOpacity>
);
}

Advanced Gestures

For swipe-to-delete, drag-and-drop, and complex interactions:

import React from 'react';
import { View, StyleSheet } from 'react-native';
import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';

export default function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);

const pan = Gesture.Pan()
.onUpdate((e) => {
translateX.value = e.translationX;
translateY.value = e.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});

const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
],
}));

return (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, animatedStyle]}>
{/* Card content */}
</Animated.View>
</GestureDetector>
);
}

const styles = StyleSheet.create({
card: {
width: 200,
height: 100,
backgroundColor: '#007AFF',
borderRadius: 12,
},
});

When to Use:

  • TouchableOpacity: Simple button presses, list item taps
  • react-native-gesture-handler: Swipe gestures, drag-and-drop, pan, pinch, rotation
  • Animated API: Combine with gestures for smooth, performant animations

Common Mistakes

Not Using StyleSheet.create

// BAD: Inline styles create new objects on every render
<View style={{ padding: 16, backgroundColor: '#fff' }}>
<Text>Content</Text>
</View>
// GOOD: StyleSheet.create optimizes styles
const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: '#fff',
},
});

<View style={styles.container}>
<Text>Content</Text>
</View>

Hardcoded Colors and Spacing

// BAD: Magic numbers everywhere
const styles = StyleSheet.create({
button: {
backgroundColor: '#007AFF',
padding: 16,
borderRadius: 8,
},
});
// GOOD: Use theme constants
const styles = StyleSheet.create({
button: {
backgroundColor: theme.colors.primary,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.md,
},
});

Code Review Checklist

Styling

  • StyleSheet.create used for all styles
  • Theme constants used instead of hardcoded values
  • Platform-specific styles handled correctly
  • Responsive design implemented with normalize/wp/hp
  • No inline styles (except for dynamic values)

Components

  • Atomic design structure followed
  • TypeScript props defined for all components
  • Reusable components in common/ directory
  • Domain components separate from generic
  • Component composition over large monolithic components

Accessibility

  • accessibilityLabel on interactive elements
  • accessibilityRole set correctly
  • Minimum touch target 48x48 dp
  • Color contrast meets WCAG AA (4.5:1)
  • Text scaling supported for large text sizes

Performance

  • StyleSheet memoized outside component
  • Large lists use FlatList, not ScrollView
  • Images optimized and properly sized
  • Unnecessary re-renders prevented
  • Heavy computations memoized with useMemo

Further Reading

React Native Framework Guidelines

Cross-Platform Guidelines

External Resources


Summary

Key Takeaways

  1. StyleSheet.create - Always use for style optimization
  2. Theme system - Centralize colors, spacing, typography
  3. Atomic design - Build from atoms to organisms to screens
  4. Platform-aware - Handle iOS/Android style differences
  5. Responsive - Normalize sizes for different screen densities
  6. Accessible - Labels, roles, minimum touch targets
  7. Reusable components - Button, Input, Card patterns
  8. Type-safe - Define props with TypeScript
  9. Performance - Memoize styles, use FlatList
  10. Consistent - Follow design system

Next Steps: Review React Native Data for API integration and offline storage patterns.