Module 4: StyleSheet Deep Dive

Responsive Design Without Media Queries

Build layouts that adapt beautifully to any screen size

Table of Contents

🎯 Learning Objectives

  • Use useWindowDimensions to create reactive layouts
  • Understand when to use Dimensions API 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)
375×667 Portrait 667×375 Landscape 768×1024 Tablet

⚠️ 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 units100% 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>;
}
1 col small <375 2 cols medium 375-413 2 cols large 414-767 3 cols tablet 768-1023 4 cols desktop 1024+

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 maxFontSizeMultiplier for UI elements that break at large sizes
  • Adjust layouts when fontScale > 1.2 to 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

Status Bar Safe Area Home Indicator top inset bottom inset

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.