Responsive Design Without Media Queries
Build layouts that adapt beautifully to any screen size
Table of Contents
🎯 Learning Objectives
- Use
useWindowDimensionsto create reactive layouts - Understand when to use
DimensionsAPI vs the hook - Build a reusable breakpoint system for your app
- Handle device orientation changes gracefully
- Adapt layouts for phones, tablets, and foldables
- Implement scalable typography that respects user preferences
- Work with safe areas across different device types
Introduction
On the web, responsive design means media queries: @media (min-width: 768px). In React Native, there are no media queries. Instead, you have something more powerful: JavaScript that can read device dimensions and compute styles dynamically.
This shift from declarative CSS breakpoints to imperative JavaScript logic might seem like a step backward, but it's actually more flexible. You can create any breakpoint system you want, respond to orientation changes in real-time, and build truly adaptive layouts that go beyond simple width checks.
📱 The Mobile Responsive Challenge
Mobile devices vary wildly: iPhone SE (375×667) to iPad Pro (1024×1366), portrait to landscape, notches and Dynamic Island, Android phones with every imaginable aspect ratio. Your layouts need to handle all of this gracefully.
What We'll Build
By the end of this lesson, you'll have a toolkit for responsive design:
- A reusable breakpoint hook that works like CSS media queries
- Responsive grid that changes columns based on screen width
- Typography that scales appropriately across devices
- Layouts that reorganize when orientation changes
flowchart LR
subgraph Input["Device Context"]
W["Screen Width"]
H["Screen Height"]
O["Orientation"]
S["Scale Factor"]
end
subgraph Logic["Responsive Logic"]
BP["Breakpoint
Detection"]
CALC["Dynamic
Calculations"]
end
subgraph Output["Adaptive UI"]
L["Layout Changes"]
T["Typography Scaling"]
G["Grid Columns"]
end
Input --> Logic
Logic --> Output
style Input fill:#e3f2fd
style Logic fill:#fff3e0
style Output fill:#e8f5e9
React Native's Responsive Tools
React Native provides several built-in tools for responsive design. Let's understand when to use each one:
| Tool | Type | Updates On Change | Best For |
|---|---|---|---|
useWindowDimensions |
Hook | ✅ Yes (reactive) | Component layouts, orientation |
Dimensions.get() |
API | ❌ No (static) | Initial values, outside components |
PixelRatio |
API | N/A (constant) | Pixel-perfect sizing, font scaling |
| Percentage strings | Style value | ✅ Yes (via layout) | Simple proportional sizing |
✅ Rule of Thumb
Use useWindowDimensions for anything that should update when the screen size changes (like rotation). Use Dimensions.get() only when you need dimensions outside a component or for one-time calculations.
useWindowDimensions Hook
The useWindowDimensions hook is your primary tool for responsive layouts. It returns the current window dimensions and automatically triggers re-renders when they change.
import { useWindowDimensions, View, Text, StyleSheet } from 'react-native';
function ResponsiveComponent() {
const { width, height } = useWindowDimensions();
// Derived values
const isLandscape = width > height;
const isTablet = width >= 768;
const columnCount = width >= 1024 ? 4 : width >= 768 ? 3 : 2;
return (
<View style={styles.container}>
<Text>Screen: {width} × {height}</Text>
<Text>Orientation: {isLandscape ? 'Landscape' : 'Portrait'}</Text>
<Text>Device: {isTablet ? 'Tablet' : 'Phone'}</Text>
<Text>Grid Columns: {columnCount}</Text>
</View>
);
}
Dynamic Styles with useWindowDimensions
You can use the dimensions to compute styles inline or in a style-generating function:
function AdaptiveCard() {
const { width } = useWindowDimensions();
// Card width adapts to screen
const cardWidth = width >= 768
? 300 // Fixed width on tablets
: width - 32; // Full width minus padding on phones
return (
<View style={[styles.card, { width: cardWidth }]}>
<Text style={styles.cardTitle}>Adaptive Card</Text>
<Text style={styles.cardBody}>
This card is {cardWidth}dp wide
</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
backgroundColor: '#fff',
borderRadius: 12,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2,
},
cardTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 8,
},
cardBody: {
fontSize: 14,
color: '#666',
},
});
When Dimensions Change
The hook re-renders your component whenever dimensions change. This happens when:
- Device rotates (portrait ↔ landscape)
- Foldable device unfolds/folds
- Split-screen multitasking on tablets
- Keyboard appears (on some Android devices)
⚠️ Performance Tip
The hook causes re-renders on dimension changes. For complex components, consider memoizing expensive calculations or splitting responsive logic into a parent component that passes props to memoized children.
Dimensions API
The Dimensions API provides screen and window measurements. Unlike the hook, it returns static values at call time.
import { Dimensions } from 'react-native';
// Get current dimensions (static snapshot)
const { width, height } = Dimensions.get('window');
const screen = Dimensions.get('screen');
console.log('Window:', width, '×', height);
console.log('Screen:', screen.width, '×', screen.height);
Window vs Screen
The API provides two measurement types:
Window
The visible area of your app. On iOS, this is the same as screen. On Android, this excludes the status bar and navigation bar.
Use for: Layout calculations
Screen
The full physical screen dimensions, including areas covered by system UI.
Use for: Full-screen overlays, splash screens
Listening to Dimension Changes
While the API itself is static, you can subscribe to changes:
import { useEffect, useState } from 'react';
import { Dimensions } from 'react-native';
// Custom hook that mimics useWindowDimensions
function useDimensions() {
const [dimensions, setDimensions] = useState(() => Dimensions.get('window'));
useEffect(() => {
const subscription = Dimensions.addEventListener('change', ({ window }) => {
setDimensions(window);
});
return () => subscription.remove();
}, []);
return dimensions;
}
💡 When to Use Dimensions API
- Creating StyleSheet outside components (initial values)
- Calculating values in utility functions
- When you explicitly don't want reactive updates
- Getting screen (not window) dimensions
Using Dimensions in StyleSheet
You can use Dimensions when defining StyleSheet, but remember these are static:
const { width } = Dimensions.get('window');
// These styles are calculated once at import time
const styles = StyleSheet.create({
halfWidth: {
width: width / 2, // Won't update on rotation!
},
responsive: {
// Better: use percentages or calculate in component
width: '50%',
},
});
Percentage-Based Layouts
The simplest form of responsive design uses percentage values. These automatically adapt to container size without any JavaScript calculation.
const styles = StyleSheet.create({
// Percentages for width
fullWidth: {
width: '100%',
},
halfWidth: {
width: '50%',
},
// Percentages for height (relative to parent)
halfHeight: {
height: '50%',
},
// Percentages for positioning
centered: {
position: 'absolute',
top: '50%',
left: '50%',
// Note: This centers the top-left corner, not the element
},
// Padding and margin also work
spacious: {
padding: '5%', // 5% of parent width
marginHorizontal: '10%',
},
});
Percentage Gotchas
⚠️ Important Limitations
- Height percentages require the parent to have a defined height
- No viewport units —
100%is relative to parent, not screen - Padding/margin percentages are based on parent width (both horizontal and vertical)
- Font sizes don't support percentages
Common Percentage Patterns
// Full-screen container
const FullScreen = ({ children }) => (
<View style={{ flex: 1 }}> {/* flex: 1 is better than height: '100%' */}
{children}
</View>
);
// Two equal columns
function TwoColumns({ left, right }) {
return (
<View style={styles.row}>
<View style={styles.column}>{left}</View>
<View style={styles.column}>{right}</View>
</View>
);
}
const styles = StyleSheet.create({
row: {
flexDirection: 'row',
gap: 16,
},
column: {
flex: 1, // Equal flex is often better than width: '50%'
},
});
// Aspect ratio maintained image
function ResponsiveImage({ source }) {
return (
<Image
source={source}
style={{
width: '100%',
aspectRatio: 16 / 9, // Maintains ratio regardless of width
}}
resizeMode="cover"
/>
);
}
Creating Breakpoint Systems
While React Native doesn't have CSS media queries, you can build your own breakpoint system. This gives you media-query-like functionality with more flexibility.
Defining Breakpoints
// breakpoints.ts
export const BREAKPOINTS = {
small: 0, // Small phones
medium: 375, // Standard phones (iPhone 12/13/14)
large: 414, // Large phones (iPhone Plus/Max)
tablet: 768, // Tablets
desktop: 1024, // Large tablets, desktop
} as const;
export type Breakpoint = keyof typeof BREAKPOINTS;
Breakpoint Hook
// useBreakpoint.ts
import { useWindowDimensions } from 'react-native';
import { BREAKPOINTS, Breakpoint } from './breakpoints';
export function useBreakpoint(): Breakpoint {
const { width } = useWindowDimensions();
if (width >= BREAKPOINTS.desktop) return 'desktop';
if (width >= BREAKPOINTS.tablet) return 'tablet';
if (width >= BREAKPOINTS.large) return 'large';
if (width >= BREAKPOINTS.medium) return 'medium';
return 'small';
}
// Additional utility hooks
export function useIsTablet(): boolean {
const { width } = useWindowDimensions();
return width >= BREAKPOINTS.tablet;
}
export function useIsPhone(): boolean {
const { width } = useWindowDimensions();
return width < BREAKPOINTS.tablet;
}
Using Breakpoints in Components
function ResponsiveGrid({ items }) {
const breakpoint = useBreakpoint();
// Different column counts per breakpoint
const columns = {
small: 1,
medium: 2,
large: 2,
tablet: 3,
desktop: 4,
}[breakpoint];
return (
<View style={styles.grid}>
{items.map((item, index) => (
<View
key={index}
style={[styles.gridItem, { width: `${100 / columns}%` }]}
>
<ItemCard item={item} />
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
gridItem: {
padding: 8,
},
});
Responsive Style Objects
Create a utility for responsive styles similar to CSS media queries:
// useResponsiveStyles.ts
import { useWindowDimensions } from 'react-native';
import { BREAKPOINTS } from './breakpoints';
import { StyleSheet, ViewStyle, TextStyle, ImageStyle } from 'react-native';
type Style = ViewStyle | TextStyle | ImageStyle;
interface ResponsiveStyles<T extends Style> {
base: T;
sm?: Partial<T>;
md?: Partial<T>;
lg?: Partial<T>;
tablet?: Partial<T>;
desktop?: Partial<T>;
}
export function useResponsiveStyle<T extends Style>(
responsiveStyles: ResponsiveStyles<T>
): T {
const { width } = useWindowDimensions();
let result = { ...responsiveStyles.base };
if (width >= BREAKPOINTS.medium && responsiveStyles.sm) {
result = { ...result, ...responsiveStyles.sm };
}
if (width >= BREAKPOINTS.large && responsiveStyles.md) {
result = { ...result, ...responsiveStyles.md };
}
if (width >= BREAKPOINTS.tablet && responsiveStyles.tablet) {
result = { ...result, ...responsiveStyles.tablet };
}
if (width >= BREAKPOINTS.desktop && responsiveStyles.desktop) {
result = { ...result, ...responsiveStyles.desktop };
}
return result as T;
}
// Usage
function ResponsiveCard() {
const cardStyle = useResponsiveStyle({
base: {
padding: 12,
borderRadius: 8,
},
tablet: {
padding: 24,
borderRadius: 16,
},
desktop: {
padding: 32,
maxWidth: 600,
},
});
return <View style={cardStyle}>...</View>;
}
Handling Orientation Changes
Device orientation affects layout significantly. Portrait and landscape modes often need different arrangements.
Detecting Orientation
import { useWindowDimensions } from 'react-native';
function useOrientation() {
const { width, height } = useWindowDimensions();
return {
isPortrait: height >= width,
isLandscape: width > height,
orientation: height >= width ? 'portrait' : 'landscape',
};
}
// Usage
function OrientationAwareScreen() {
const { isPortrait, isLandscape } = useOrientation();
return (
<View style={[
styles.container,
isLandscape && styles.containerLandscape,
]}>
{/* Content adapts to orientation */}
</View>
);
}
Layout Changes by Orientation
function MediaPlayer({ video, controls }) {
const { isLandscape } = useOrientation();
if (isLandscape) {
// Landscape: video fills screen, controls overlay
return (
<View style={styles.fullScreen}>
<Video source={video} style={StyleSheet.absoluteFill} />
<View style={styles.overlayControls}>
{controls}
</View>
</View>
);
}
// Portrait: video top, controls bottom
return (
<View style={styles.container}>
<Video source={video} style={styles.videoPortrait} />
<View style={styles.controlsBelow}>
{controls}
</View>
</View>
);
}
const styles = StyleSheet.create({
fullScreen: {
flex: 1,
},
container: {
flex: 1,
},
videoPortrait: {
width: '100%',
aspectRatio: 16 / 9,
},
overlayControls: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 16,
},
controlsBelow: {
flex: 1,
padding: 16,
},
});
Sidebar Pattern for Landscape
A common pattern: show content in a list on portrait, show sidebar + detail on landscape.
function MasterDetailLayout({ items, selectedItem, onSelect, renderDetail }) {
const { isLandscape } = useOrientation();
const [localSelected, setLocalSelected] = useState(selectedItem);
const handleSelect = (item) => {
setLocalSelected(item);
onSelect?.(item);
};
if (isLandscape) {
// Side-by-side layout
return (
<View style={styles.splitView}>
<View style={styles.sidebar}>
<ItemList items={items} onSelect={handleSelect} />
</View>
<View style={styles.detail}>
{localSelected ? renderDetail(localSelected) : (
<EmptyState message="Select an item" />
)}
</View>
</View>
);
}
// Portrait: show list or detail (use navigation)
return (
<View style={styles.container}>
<ItemList items={items} onSelect={handleSelect} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
splitView: {
flex: 1,
flexDirection: 'row',
},
sidebar: {
width: 320,
borderRightWidth: 1,
borderRightColor: '#e0e0e0',
},
detail: {
flex: 1,
},
});
Adapting for Device Types
Phones, tablets, and foldables each have different interaction patterns and content density expectations.
Device Detection Hook
import { useWindowDimensions, Platform } from 'react-native';
interface DeviceInfo {
isPhone: boolean;
isTablet: boolean;
isSmallPhone: boolean;
isLargePhone: boolean;
deviceType: 'phone' | 'tablet';
}
export function useDeviceType(): DeviceInfo {
const { width, height } = useWindowDimensions();
const shortDimension = Math.min(width, height);
const longDimension = Math.max(width, height);
// Tablets typically have shortest dimension >= 600dp
// and aspect ratio closer to square
const isTablet = shortDimension >= 600;
return {
isPhone: !isTablet,
isTablet,
isSmallPhone: !isTablet && shortDimension < 375,
isLargePhone: !isTablet && shortDimension >= 414,
deviceType: isTablet ? 'tablet' : 'phone',
};
}
Tablet-Optimized Layouts
Tablets can display more content and use different navigation patterns:
function AppLayout({ children }) {
const { isTablet } = useDeviceType();
if (isTablet) {
return (
<View style={styles.tabletLayout}>
<View style={styles.tabletSidebar}>
<NavigationMenu />
</View>
<View style={styles.tabletContent}>
{children}
</View>
</View>
);
}
// Phone: bottom tab navigation
return (
<View style={styles.phoneLayout}>
<View style={styles.phoneContent}>
{children}
</View>
<BottomTabBar />
</View>
);
}
const styles = StyleSheet.create({
tabletLayout: {
flex: 1,
flexDirection: 'row',
},
tabletSidebar: {
width: 280,
backgroundColor: '#f5f5f5',
borderRightWidth: 1,
borderRightColor: '#e0e0e0',
},
tabletContent: {
flex: 1,
},
phoneLayout: {
flex: 1,
},
phoneContent: {
flex: 1,
},
});
Content Density
Tablets can show more information at once. Adjust your content density:
function ProductList({ products }) {
const { isTablet } = useDeviceType();
const { width } = useWindowDimensions();
// More columns on tablet
const numColumns = isTablet ? 3 : 2;
// Larger items on tablet
const itemSpacing = isTablet ? 16 : 8;
const itemWidth = (width - itemSpacing * (numColumns + 1)) / numColumns;
return (
<FlatList
data={products}
numColumns={numColumns}
key={numColumns} // Force re-render when columns change
contentContainerStyle={{ padding: itemSpacing }}
columnWrapperStyle={{ gap: itemSpacing }}
ItemSeparatorComponent={() => <View style={{ height: itemSpacing }} />}
renderItem={({ item }) => (
<ProductCard
product={item}
width={itemWidth}
// Larger text on tablet
titleSize={isTablet ? 18 : 14}
priceSize={isTablet ? 20 : 16}
/>
)}
/>
);
}
Scalable Units and Typography
Creating consistent sizing across different screen sizes requires understanding density-independent units and font scaling.
PixelRatio API
import { PixelRatio, Dimensions } from 'react-native';
// Get device pixel density
const pixelRatio = PixelRatio.get();
// iPhone: 2 or 3, Android varies: 1, 1.5, 2, 3, 4
// Get font scale (user accessibility setting)
const fontScale = PixelRatio.getFontScale();
// 1.0 = default, >1 = larger text, <1 = smaller
// Round to nearest pixel
const roundedSize = PixelRatio.roundToNearestPixel(10.5);
// Ensures crisp rendering
console.log('Pixel Ratio:', pixelRatio);
console.log('Font Scale:', fontScale);
Scaled Typography
Create a typography system that scales with screen size:
import { Dimensions, PixelRatio } from 'react-native';
const { width: SCREEN_WIDTH } = Dimensions.get('window');
// Base width for scaling (iPhone 14 as reference)
const BASE_WIDTH = 390;
// Scale factor based on screen width
const scale = SCREEN_WIDTH / BASE_WIDTH;
// Scale a value with min/max bounds
export function scaleSize(size: number, factor = 0.5): number {
const scaledSize = size + (size * (scale - 1) * factor);
return PixelRatio.roundToNearestPixel(scaledSize);
}
// Typography scales
export const typography = {
// Headings scale more
h1: scaleSize(32),
h2: scaleSize(24),
h3: scaleSize(20),
// Body text scales less
body: scaleSize(16, 0.3),
bodySmall: scaleSize(14, 0.3),
// Captions barely scale
caption: scaleSize(12, 0.2),
};
// Usage
const styles = StyleSheet.create({
heading: {
fontSize: typography.h1,
fontWeight: 'bold',
},
body: {
fontSize: typography.body,
lineHeight: typography.body * 1.5,
},
});
Respecting User Font Size Preferences
Users can adjust system font size for accessibility. Your app should respect this:
import { PixelRatio, Text, StyleSheet } from 'react-native';
// Check if user has increased font size
const fontScale = PixelRatio.getFontScale();
const hasLargeText = fontScale > 1.2;
// Adjust layouts for large text
function AccessibleCard({ title, description }) {
return (
<View style={[
styles.card,
hasLargeText && styles.cardLargeText,
]}>
<Text
style={styles.title}
// Allow text to scale with system settings
allowFontScaling={true}
// Or limit maximum scaling
maxFontSizeMultiplier={1.5}
>
{title}
</Text>
<Text style={styles.description}>
{description}
</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
flexDirection: 'row',
padding: 16,
},
cardLargeText: {
// Stack vertically when text is large
flexDirection: 'column',
},
title: {
fontSize: 16,
},
description: {
fontSize: 14,
},
});
✅ Accessibility Best Practices
- Always test with font scaling set to maximum
- Use
allowFontScaling={true}(default) for body text - Consider
maxFontSizeMultiplierfor UI elements that break at large sizes - Adjust layouts when
fontScale > 1.2to prevent overflow
Safe Areas and Notches
Modern devices have notches, Dynamic Island, rounded corners, and home indicators. Safe areas ensure your content doesn't hide behind these elements.
Understanding Safe Areas
Using react-native-safe-area-context
The recommended way to handle safe areas in Expo and React Native:
# Install
npx expo install react-native-safe-area-context
// App.tsx - Wrap your app
import { SafeAreaProvider } from 'react-native-safe-area-context';
export default function App() {
return (
<SafeAreaProvider>
<Navigation />
</SafeAreaProvider>
);
}
SafeAreaView Component
import { SafeAreaView } from 'react-native-safe-area-context';
function Screen() {
return (
<SafeAreaView style={styles.container}>
{/* Content is inset from notch and home indicator */}
<Text>Safe content here</Text>
</SafeAreaView>
);
}
// Control which edges to apply
function CustomScreen() {
return (
<SafeAreaView
style={styles.container}
edges={['top', 'left', 'right']} // Exclude bottom
>
{/* Bottom edge extends to screen edge */}
</SafeAreaView>
);
}
useSafeAreaInsets Hook
For fine-grained control, use the insets directly:
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function CustomHeader() {
const insets = useSafeAreaInsets();
return (
<View style={[
styles.header,
{ paddingTop: insets.top + 12 } // Add to safe area
]}>
<Text style={styles.title}>Header</Text>
</View>
);
}
function BottomSheet() {
const insets = useSafeAreaInsets();
return (
<View style={[
styles.sheet,
{ paddingBottom: Math.max(insets.bottom, 16) }
]}>
{/* Content */}
</View>
);
}
// All available insets
function DebugInsets() {
const insets = useSafeAreaInsets();
console.log({
top: insets.top, // Notch/status bar
bottom: insets.bottom, // Home indicator
left: insets.left, // Landscape notch
right: insets.right, // Landscape notch
});
return null;
}
Safe Areas in Different Contexts
Full-Screen Content
// Image extends to edges
// Controls stay in safe area
<View style={styles.fullScreen}>
<Image
source={image}
style={StyleSheet.absoluteFill}
/>
<SafeAreaView style={styles.overlay}>
<Controls />
</SafeAreaView>
</View>
Scroll Content
// Content scrolls under notch
// with proper padding
<ScrollView
contentContainerStyle={{
paddingTop: insets.top,
paddingBottom: insets.bottom,
}}
>
{/* Scrollable content */}
</ScrollView>
Keyboard Avoiding with Safe Areas
import { KeyboardAvoidingView, Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function ChatScreen() {
const insets = useSafeAreaInsets();
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={insets.top}
>
<MessageList />
<View style={{ paddingBottom: insets.bottom }}>
<MessageInput />
</View>
</KeyboardAvoidingView>
);
}
Real-World Responsive Patterns
Let's combine everything into practical, production-ready patterns.
Pattern: Responsive Product Grid
import { useWindowDimensions, FlatList, View, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function ProductGrid({ products }) {
const { width } = useWindowDimensions();
const insets = useSafeAreaInsets();
// Responsive columns
const numColumns = width >= 1024 ? 4 : width >= 768 ? 3 : 2;
const spacing = width >= 768 ? 16 : 12;
const horizontalPadding = Math.max(insets.left, spacing);
const itemWidth = (width - horizontalPadding * 2 - spacing * (numColumns - 1)) / numColumns;
return (
<FlatList
data={products}
numColumns={numColumns}
key={`grid-${numColumns}`}
contentContainerStyle={{
paddingTop: spacing,
paddingBottom: insets.bottom + spacing,
paddingHorizontal: horizontalPadding,
}}
columnWrapperStyle={{ gap: spacing }}
ItemSeparatorComponent={() => <View style={{ height: spacing }} />}
renderItem={({ item }) => (
<ProductCard product={item} width={itemWidth} />
)}
/>
);
}
Pattern: Adaptive Navigation
function AppShell({ children }) {
const { width } = useWindowDimensions();
const insets = useSafeAreaInsets();
const isWideScreen = width >= 768;
if (isWideScreen) {
// Sidebar navigation for tablets/desktops
return (
<View style={styles.wideContainer}>
<View style={[styles.sidebar, { paddingTop: insets.top }]}>
<SidebarNav />
</View>
<View style={styles.mainContent}>
{children}
</View>
</View>
);
}
// Bottom tab navigation for phones
return (
<View style={styles.phoneContainer}>
<View style={[styles.content, { paddingTop: insets.top }]}>
{children}
</View>
<View style={{ paddingBottom: insets.bottom }}>
<BottomTabNav />
</View>
</View>
);
}
const styles = StyleSheet.create({
wideContainer: {
flex: 1,
flexDirection: 'row',
},
sidebar: {
width: 280,
backgroundColor: '#f8f9fa',
borderRightWidth: 1,
borderRightColor: '#e0e0e0',
},
mainContent: {
flex: 1,
},
phoneContainer: {
flex: 1,
},
content: {
flex: 1,
},
});
Pattern: Responsive Form Layout
function SignupForm() {
const { width } = useWindowDimensions();
const isWide = width >= 600;
return (
<ScrollView contentContainerStyle={styles.form}>
{/* Name fields side-by-side on wide screens */}
<View style={[styles.row, !isWide && styles.stack]}>
<View style={[styles.field, isWide && styles.halfField]}>
<LabeledInput label="First Name" />
</View>
<View style={[styles.field, isWide && styles.halfField]}>
<LabeledInput label="Last Name" />
</View>
</View>
{/* Email always full width */}
<View style={styles.field}>
<LabeledInput label="Email" />
</View>
{/* Address fields */}
<View style={[styles.row, !isWide && styles.stack]}>
<View style={[styles.field, isWide && styles.wideField]}>
<LabeledInput label="City" />
</View>
<View style={[styles.field, isWide && styles.narrowField]}>
<LabeledInput label="State" />
</View>
<View style={[styles.field, isWide && styles.narrowField]}>
<LabeledInput label="ZIP" />
</View>
</View>
</ScrollView>
);
}
const styles = StyleSheet.create({
form: {
padding: 16,
maxWidth: 800,
alignSelf: 'center',
width: '100%',
},
row: {
flexDirection: 'row',
gap: 16,
},
stack: {
flexDirection: 'column',
gap: 0,
},
field: {
marginBottom: 20,
},
halfField: {
flex: 1,
},
wideField: {
flex: 2,
},
narrowField: {
flex: 1,
},
});
Pattern: Responsive Modal/Dialog
function ResponsiveModal({ visible, onClose, children }) {
const { width, height } = useWindowDimensions();
const insets = useSafeAreaInsets();
const isPhone = width < 600;
// On phones: bottom sheet
// On tablets: centered modal
const modalStyle = isPhone
? {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
maxHeight: height * 0.9,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
paddingBottom: insets.bottom,
}
: {
width: Math.min(500, width - 48),
maxHeight: height - 100,
borderRadius: 16,
alignSelf: 'center',
};
if (!visible) return null;
return (
<View style={styles.overlay}>
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
<View style={[styles.modal, modalStyle]}>
{children}
</View>
</View>
);
}
const styles = StyleSheet.create({
overlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
},
modal: {
backgroundColor: '#fff',
padding: 20,
},
});
Hands-On Exercises
Exercise 1: Create a Breakpoint Hook
Build a custom useBreakpoint hook that returns 'xs', 'sm', 'md', 'lg', or 'xl' based on screen width.
Show Solution
import { useWindowDimensions } from 'react-native';
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
const breakpoints = {
xs: 0,
sm: 375,
md: 428,
lg: 768,
xl: 1024,
};
export function useBreakpoint(): Breakpoint {
const { width } = useWindowDimensions();
if (width >= breakpoints.xl) return 'xl';
if (width >= breakpoints.lg) return 'lg';
if (width >= breakpoints.md) return 'md';
if (width >= breakpoints.sm) return 'sm';
return 'xs';
}
// Bonus: helper to check minimum breakpoint
export function useMinBreakpoint(minBreakpoint: Breakpoint): boolean {
const { width } = useWindowDimensions();
return width >= breakpoints[minBreakpoint];
}
// Usage
function MyComponent() {
const breakpoint = useBreakpoint();
const isTabletOrLarger = useMinBreakpoint('lg');
console.log(`Current: ${breakpoint}, Tablet+: ${isTabletOrLarger}`);
}
Exercise 2: Build a Responsive Card Grid
Create a grid that shows 1 column on small phones, 2 columns on regular phones, 3 columns on tablets, and 4 columns on large screens.
Show Solution
import { useWindowDimensions, View, StyleSheet } from 'react-native';
function ResponsiveCardGrid({ items, renderCard }) {
const { width } = useWindowDimensions();
const getColumns = () => {
if (width >= 1024) return 4;
if (width >= 768) return 3;
if (width >= 375) return 2;
return 1;
};
const columns = getColumns();
const gap = 12;
const padding = 16;
const cardWidth = (width - padding * 2 - gap * (columns - 1)) / columns;
return (
<View style={[styles.grid, { padding }]}>
{items.map((item, index) => (
<View
key={item.id ?? index}
style={[
styles.cardWrapper,
{ width: cardWidth },
index % columns !== columns - 1 && { marginRight: gap },
]}
>
{renderCard(item, cardWidth)}
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
},
cardWrapper: {
marginBottom: 12,
},
});
Exercise 3: Handle Orientation Change
Create a video player component that shows controls below the video in portrait but overlays controls in landscape.
Show Solution
import { useWindowDimensions, View, StyleSheet } from 'react-native';
function VideoPlayer({ videoSource }) {
const { width, height } = useWindowDimensions();
const isLandscape = width > height;
return (
<View style={styles.container}>
<View style={isLandscape ? styles.fullscreenVideo : styles.portraitVideo}>
<Video source={videoSource} style={StyleSheet.absoluteFill} />
{isLandscape && (
<View style={styles.overlayControls}>
<PlayerControls />
</View>
)}
</View>
{!isLandscape && (
<View style={styles.belowControls}>
<PlayerControls />
<VideoInfo />
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
fullscreenVideo: {
flex: 1,
},
portraitVideo: {
width: '100%',
aspectRatio: 16 / 9,
},
overlayControls: {
...StyleSheet.absoluteFillObject,
justifyContent: 'flex-end',
padding: 16,
backgroundColor: 'rgba(0,0,0,0.3)',
},
belowControls: {
flex: 1,
padding: 16,
backgroundColor: '#fff',
},
});
Summary
You now have a complete toolkit for building responsive React Native apps without CSS media queries.
🎯 Key Takeaways
- useWindowDimensions is your primary tool — it's reactive and updates on changes
- Breakpoint systems can be built with simple JavaScript logic
- Orientation is just comparing width vs height
- Tablets need different layouts, navigation, and content density
- Typography should scale with screen size and respect user preferences
- Safe areas are essential for modern devices with notches
Quick Reference
// Essential imports
import { useWindowDimensions, Dimensions, PixelRatio } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
// Get dimensions (reactive)
const { width, height } = useWindowDimensions();
// Orientation
const isLandscape = width > height;
// Device type
const isTablet = Math.min(width, height) >= 600;
// Safe areas
const insets = useSafeAreaInsets();
// Breakpoints
const breakpoint =
width >= 1024 ? 'desktop' :
width >= 768 ? 'tablet' :
width >= 414 ? 'large' :
width >= 375 ? 'medium' : 'small';
// Font scaling
const fontScale = PixelRatio.getFontScale();
Coming Up Next
In the next lesson, we'll explore Platform-Specific Styles. You'll learn how to write styles that work differently on iOS and Android, handle platform quirks, and create a consistent cross-platform experience.