with clean responsive layouts
A phone layout that is simply scaled up on an iPad wastes screen space, and it looks broken on any foldable. With useWindowDimensions, proper safe area handling and master-detail patterns, you can build responsive layouts that work equally well on phone, tablet and unfolded foldable screens.
Table of Contents
- 1. Why phone-first layouts break on tablet and foldable
- 2. Detecting screen size: useWindowDimensions vs. Dimensions.get
- 3. Safe area handling for notches and multitasking insets
- 4. Adaptive layout patterns: master-detail, grid, navigation
- 5. iPad multi-window: Slide Over, Split View, Stage Manager
- 6. Foldable support on Android: the hinge and Jetpack WindowManager
- 7. Orientation handling without losing scroll or form state
- 8. Responsive typography and spacing instead of fixed values
- 9. Testing strategy for tablet and foldable layouts
- 10. Summary
- 11. FAQ
1. Why phone-first layouts break on tablet and foldable
Most React Native apps start life as a phone-first layout: a single column, fixed pixel values for padding and image heights, touch targets sized for one-handed thumb use. As soon as the same app runs on an 11-inch iPad or an unfolded foldable, the problem shows immediately. A product list that fills a single column reasonably on the phone leaves two thirds of a tablet screen empty, or stretches text lines unnaturally wide. Responsive layouts are not a cosmetic add-on here, they are the precondition for an app to feel usable on larger screens at all.
Fixed pixel values are the root of the problem. A container with width: 320 or an image with a fixed height of 200 pixels implicitly assumes that available space is limited. On a large tablet, or on an unfolded foldable, suddenly two or three times as much horizontal space is available, and a single-column layout wastes exactly that space. Real tablet and foldable support requires layout decisions to be made relative to available space, not relative to an assumed phone viewport.
Touch targets are an underrated detail too. A button optimized for one-handed use near the bottom edge of a phone screen may sit outside the comfortable reach of a thumb on a 12.9-inch iPad, especially while the user holds a keyboard or an Apple Pencil in the other hand. Interaction patterns that feel natural on a phone demand a deliberate rethink on larger screens, both in where primary actions are placed and in how large tap areas need to be.
2. Detecting screen size: useWindowDimensions vs. Dimensions.get
React Native offers two ways to read the current window size: the imperative Dimensions.get('window') API and the useWindowDimensions() hook. The decisive difference is re-render behavior. Dimensions.get returns a snapshot at the moment it is called and does not automatically update the component when window size changes. On rotation, on folding or unfolding a foldable, or on a size change inside iPad Split View, the component stays stuck on the old value until it re-renders for some unrelated reason.
useWindowDimensions, by contrast, subscribes internally to the change event of Dimensions and automatically triggers a re-render on every change. For responsive layouts that need to react to rotation, fold events and multitasking size changes, the hook is nearly always the right choice. A central breakpoint strategy can be cleanly encapsulated as its own hook that returns a category based on current width, instead of repeating individual number comparisons in every component.
It matters to align breakpoints with real layout needs rather than device classes. A foldable in its folded state behaves layout-wise like a narrow phone, in its unfolded state like a small tablet, independent of the device's marketing name. Three categories cover most apps: phone up to roughly 600 logical pixels, tablet between 600 and 900, large tablets and unfolded foldables above that.
// hooks/useBreakpoint.js
// Custom hook wrapping useWindowDimensions with a stable breakpoint API
import { useWindowDimensions } from 'react-native';
const BREAKPOINTS = {
phone: 0,
tablet: 600,
largeTablet: 900,
};
export function useBreakpoint() {
// useWindowDimensions re-renders on rotation, fold and multitasking resize
const { width, height } = useWindowDimensions();
const isTablet = width >= BREAKPOINTS.tablet;
const isLargeTablet = width >= BREAKPOINTS.largeTablet;
const isLandscape = width > height;
let breakpoint = 'phone';
if (isLargeTablet) breakpoint = 'largeTablet';
else if (isTablet) breakpoint = 'tablet';
return { width, height, breakpoint, isTablet, isLargeTablet, isLandscape };
}
// Usage in a component
function ProductGrid() {
const { breakpoint, isTablet } = useBreakpoint();
const columns = breakpoint === 'largeTablet' ? 4 : breakpoint === 'tablet' ? 3 : 1;
return <ProductList numColumns={columns} showSidebar={isTablet} />;
}
3. Safe area handling for notches and multitasking insets
Safe area insets are mostly a footnote on phones: a notch on top, a home indicator bar at the bottom. On tablets and in iPad multitasking they become an active layout factor. react-native-safe-area-context exposes current insets through useSafeAreaInsets(), and the crucial point for tablet and foldable support is that these values change dynamically as the app moves in and out of Split View or Slide Over, not only once at app launch.
In iPad Split View, an app suddenly gets a narrower viewport, while rounded corners and any notch area still exist relative to the whole screen, not relative to the reduced app window. An app running in Slide Over as a floating window above another app additionally has rounded corners on all four sides that do not exist in full-screen view. Reading insets once on mount and treating them as a constant afterward produces visibly mispositioned headers and footers exactly during these transitions.
Foldables add another case: in the folded state with the cover display, different insets apply than in the unfolded state with the inner display, and the transition between the two often happens without a full remount of the app. The SafeAreaProvider therefore needs to sit high enough in the tree to re-measure on every relevant layout change, and components should consume insets through the hook instead of duplicating them in local state.
4. Adaptive layout patterns: master-detail, grid, navigation
The master-detail pattern is the single most important tool for responsive layouts in list-detail applications. On phone, a list is shown, and a tap navigates via a stack push to a dedicated detail screen. From a defined breakpoint onward, typically the tablet breakpoint, the same view switches to a split-view presentation: list on the left, detail on the right, both visible at once, with no navigation involved. The challenge is implementing this switch without duplicated code paths, but rather through a shared state source that is interpreted either as a navigation push or as a parallel presentation depending on the current breakpoint.
Responsive grid systems solve the analogous problem for card and product lists: the number of columns depends on available width, not on a fixed constant. A grid with a single column on phone, three columns on tablet and four to five columns on a large, unfolded foldable uses space proportionally without stretching individual cards unnaturally. It matters to calculate column count from width and a minimum card size, rather than hardcoding a fixed value per breakpoint.
Conditional navigation complements both patterns: on phone, stack navigation with full-screen screens remains sensible, while on tablet a permanently visible sidebar or drawer can take over navigation because enough horizontal space exists to show navigation and content simultaneously. The navigation library must support this switch without losing the entire navigation state when the breakpoint is crossed.
// components/MasterDetailLayout.jsx
// Responsive master-detail layout: stack navigation on phone, split view on tablet
import { View, StyleSheet } from 'react-native';
import { useBreakpoint } from '../hooks/useBreakpoint';
export function MasterDetailLayout({ list, selectedId, onSelect, renderDetail }) {
const { isTablet } = useBreakpoint();
if (!isTablet) {
// Phone: only list or only detail is visible, navigation handles the rest
return selectedId
? <View style={styles.flexFill}>{renderDetail(selectedId)}</View>
: <View style={styles.flexFill}>{list}</View>;
}
// Tablet and larger: list and detail side by side
return (
<View style={styles.row}>
<View style={styles.listPane}>
{list}
</View>
<View style={styles.flexFill}>
{selectedId ? renderDetail(selectedId) : <EmptyDetailState />}
</View>
</View>
);
}
const styles = StyleSheet.create({
flexFill: { flex: 1 },
row: { flex: 1, flexDirection: 'row' },
listPane: { width: 360, borderRightWidth: 1, borderColor: '#e2e8f0' },
});
5. iPad multi-window: Slide Over, Split View, Stage Manager
Multi-window on iPadOS brings three relevant modes: Split View, where two apps run side by side and the available space can be adjusted via a drag handle, Slide Over, where an app floats as a smaller window above another app, and Stage Manager, which allows several freely resizable windows at once. For tablet and foldable support, this means the app's window size can change at any moment, without a user actively restarting the app or rotating a device.
The practical consequence: an app cannot assume that current window size stays stable, and it must not react to a size change with a full remount. A remount loses scroll positions, form input and navigation state, which feels extremely disruptive in a Slide Over window a user briefly pulls up while working in another app. Layout decisions must therefore be made purely from the current width returned by useWindowDimensions, while the underlying component tree stays stable.
Stage Manager sharpens the problem further, because users can freely drag windows to arbitrary sizes, and the app must respond in real time to any intermediate size, not just a fixed set of standard sizes. A layout tested against only three fixed breakpoints often shows clipped text or overlapping elements at these intermediate states. Continuous, proportional scaling instead of hard jumps at individual pixel values reduces this risk considerably.
6. Foldable support on Android: the hinge and Jetpack WindowManager
Android foldables bring a layout problem that does not exist on iPad: the hinge, the physical fold area that on some devices splits the display into two visual halves, or creates a dead zone where content should not be placed. In addition, an app needs to know the current fold posture, whether flat and open, half-open in tabletop mode, or fully folded, because meaningful layout decisions follow from that, such as a split layout in tabletop mode with controls at the bottom and content on top.
React Native core does not expose this information. Android's Jetpack WindowManager library provides fold geometry, fold state and posture through WindowInfoTracker, but accessing it requires a native bridge, because these APIs exist only at the Kotlin or Java level. A native module reads fold data through a flow listener and sends it as an event to the JavaScript side, where it combines with the results from useWindowDimensions to determine the final layout decision. For real tablet and foldable support on Android, there is no way around this native bridge.
Continuity across the fold transition is the second critical point. When a user folds or unfolds a device during use, the app must not lose navigation state or form input, even though the window configuration technically changes underneath it. The native listener should therefore serve purely as a data source for layout decisions and should never trigger a reload or remount of the React Native instance.
// android/app/src/main/java/com/example/app/FoldStateModule.kt
// Native module bridging Jetpack WindowManager fold state to JavaScript
package com.example.app
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.FoldingFeature
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlinx.coroutines.*
class FoldStateModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun getName() = "FoldStateModule"
@ReactMethod
fun startObserving() {
val activity = currentActivity ?: return
val tracker = WindowInfoTracker.getOrCreate(activity)
scope.launch {
tracker.windowLayoutInfo(activity).collect { layoutInfo ->
val feature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
val payload = Arguments.createMap().apply {
putBoolean("isSeparating", feature?.isSeparating ?: false)
putString("orientation", feature?.orientation?.toString() ?: "NONE")
// FLAT, HALF_OPENED, or NONE when no folding feature is present
putString("state", feature?.state?.toString() ?: "FLAT")
}
sendEvent("foldStateChanged", payload)
}
}
}
private fun sendEvent(eventName: String, params: WritableMap) {
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}
}
7. Orientation handling without losing scroll or form state
Not every screen benefits from free rotation. A camera screen or a video player can reasonably lock to landscape, while a form or list view should work in both orientations, especially on tablet and unfolded foldable, where landscape is often the more natural state. The decision between locking and allowing rotation should therefore be made per screen, not globally for the whole app, because a global lock on large screens undermines exactly the responsive layouts that make tablet use worthwhile.
The technically tricky part is what happens to existing state during an actual rotation. React Native does not automatically re-render components on an orientation change unless a remount is triggered, but poorly written layout logic that conditionally unmounts and remounts components based on orientation checks loses scroll positions in lists and uncontrolled form input that is not held in a higher-level state.
The robust solution is to keep form state fundamentally outside the layout component, for example in a form state management library or in a context that is independent of orientation, and to track scroll positions via onScroll into a ref rather than local state, so they are not reset by a re-render triggered for unrelated reasons. The layout structure itself may change on rotation, but the underlying data and input must remain intact regardless.
8. Responsive typography and spacing instead of fixed values
Fixed font sizes and spacing are the same problem as fixed pixel widths, just at a finer grain. Body text at 16 pixels reads comfortably on phone, but feels uncomfortably small on a large tablet held at arm's length. Instead of hardcoding individual values in every component, a central token system should define font sizes and spacing per breakpoint, so a component references a semantic value like spacing.md and the concrete number resolves based on the active breakpoint.
A common mistake is coupling this decision to device detection rather than actual screen size, for example via Platform.isPad. The flag only distinguishes iPhone from iPad, but knows neither Android tablets nor the actual current window size in multitasking or on a foldable. An iPad in Slide Over mode effectively has a phone-sized window width, but a Platform.isPad check would still apply tablet typography and render text clipped or overlapping.
The correct foundation remains the actual width from useWindowDimensions, combined with the same breakpoint system used for grid columns and navigation. This way, font size, line height and spacing scale consistently with the rest of the layout, and a token-based system lets you maintain these values in one place instead of hardcoding them across dozens of components.
{
"breakpoints": {
"phone": 0,
"tablet": 600,
"largeTablet": 900
},
"spacing": {
"phone": { "xs": 4, "sm": 8, "md": 16, "lg": 24, "xl": 32 },
"tablet": { "xs": 6, "sm": 12, "md": 24, "lg": 36, "xl": 48 },
"largeTablet": { "xs": 8, "sm": 16, "md": 32, "lg": 48, "xl": 64 }
},
"typography": {
"phone": { "body": 16, "heading": 22, "caption": 12, "lineHeight": 1.4 },
"tablet": { "body": 17, "heading": 26, "caption": 13, "lineHeight": 1.45 },
"largeTablet": { "body": 18, "heading": 30, "caption": 14, "lineHeight": 1.5 }
}
}
9. Testing strategy for tablet and foldable layouts
Responsive layouts tested on a single simulator only give a false sense of security. On iOS, a realistic test plan covers at least three simulator sizes: a compact iPhone, an 11-inch iPad and a 12.9-inch iPad, each also checked in Split View and Slide Over mode, because effective window size changes there independent of the physical device. Portrait-only testing on the largest iPad is not enough to uncover multitasking bugs.
On Android, the Android Studio Device Manager, or avdmanager directly, can create a foldable emulator profile modeled after the Galaxy Fold form factor, capable of simulating both the folded and unfolded state. A separate classic 10-inch tablet profile is also worth adding, to make sure breakpoint logic holds up correctly even without fold-specific APIs, since not every Android tablet has a hinge.
Visual regression tests carry a specific risk with multi-window resizing: a screenshot test covering only a fixed window size misses layout breaks at intermediate sizes, which arise, for example, from Stage Manager or from dragging the Split View slider. A robust test approach renders the same component at several representative widths, at least at the breakpoint boundaries themselves as well as just above and below, to catch rendering jumps early rather than discovering them during manual QA.
#!/usr/bin/env bash
# Create and launch Android tablet and foldable emulator profiles for testing
set -euo pipefail
# Foldable profile similar to the Galaxy Fold form factor
avdmanager create avd \
--name "foldable_test" \
--package "system-images;android-34;google_apis;x86_64" \
--device "7.6in Foldable" \
--force
# Classic 10-inch tablet profile, no hinge, tests breakpoints alone
avdmanager create avd \
--name "tablet_10in_test" \
--package "system-images;android-34;google_apis;x86_64" \
--device "10.1in WXGA (Tablet)" \
--force
# Launch the foldable emulator, starting in the unfolded state
emulator -avd foldable_test -feature WindowManager -no-snapshot &
# Toggle fold state at runtime via emulator console
adb emu fold
adb emu unfold
| Breakpoint | Navigation pattern | Content layout | Typical screens |
|---|---|---|---|
| Phone (< 600 dp) | Stack navigation, one screen per view | Single column, full width | Phone portrait, foldable closed |
| Tablet (600-899 dp) | Master-detail split, sidebar optional | 2-3 column grid | iPad Split View, Android tablet portrait |
| Large tablet (≥ 900 dp) | Permanent sidebar/drawer, master-detail fixed | 3-5 column grid | iPad landscape, foldable unfolded |
| Detection API | Dimensions.get(): no re-render |
useWindowDimensions(): reacts live |
Rotation, fold, multitasking resize |
Mironsoft
React Native development for phone, tablet and foldable
Need a React Native app that works on every form factor?
We audit existing React Native layouts, identify phone-first assumptions, and build real responsive layouts with a breakpoint strategy, safe area handling and a native foldable bridge.
Layout audit
Analysis of existing screens for fixed pixel values and phone-first assumptions
Breakpoint system
Implementing useWindowDimensions, a token system and master-detail patterns
Foldable bridge
Native integration of Jetpack WindowManager for fold state and posture
10. Summary
Real tablet and foldable support in React Native does not come from scaling up a phone layout afterward, it comes from deliberate decisions at several levels at once: useWindowDimensions instead of Dimensions.get for layouts that react live, a breakpoint system tied to actual screen widths rather than device classes, consistent safe area handling across multitasking transitions, and master-detail patterns that switch navigation and content layout depending on available space.
On Android, the hinge and the Jetpack WindowManager APIs add another layer that is only reachable through a native bridge, but remains essential for clean foldable support. Responsive typography and spacing through a token system instead of fixed values, orientation-independent form and scroll state, and a testing strategy covering tablet simulators, foldable emulators and multitasking intermediate sizes round out a setup that makes responsive layouts reliable, not just on paper, but in real daily use.
Tablet and Foldable Support in React Native - Key Takeaways
Detecting screen size
useWindowDimensions instead of Dimensions.get: reacts automatically to rotation, fold and multitasking resize.
Adaptive layout patterns
Master-detail split from the tablet breakpoint, responsive grid columns, navigation that switches by available space.
Android foldable bridge
Jetpack WindowManager delivers fold state and posture only through a native Kotlin bridge to JavaScript.
Testing coverage
Check iPad simulators with Split View, an Android foldable emulator, and intermediate sizes for visual regression.