04 · Flexbox Layout the React Native Way¶
Every View in React Native is a flex container. There is no display: block, no floats, no
inline flow (outside Text) and no CSS Grid. That sounds limiting, but Flexbox plus absolute
positioning is enough for every mobile layout you will build — once you know where React Native's
defaults differ from the web's.
The defaults that differ from CSS¶
| Property | Web default | React Native default |
|---|---|---|
display |
block / inline |
always flex |
flexDirection |
row |
column |
alignContent |
normal (stretch) |
flex-start |
flexShrink |
1 |
0 |
position |
static |
relative |
| box sizing | content-box |
border-box behaviour (borders and padding are inside width) |
The column default exists because phones are tall: most screens stack things vertically. The
flexShrink: 0 default bites people who come from the web: a long row of items will overflow
rather than shrink unless you opt in.
flex: 1 — the most important line you'll write¶
In React Native, flex takes a single number. flex: 1 means "grow to fill the available space
along the main axis, and allow shrinking."
<View style={{ flex: 1 }}> {/* fills the screen */}
<View style={{ height: 56 }} /> {/* fixed header */}
<View style={{ flex: 1 }} /> {/* body takes everything left */}
<View style={{ height: 64 }} /> {/* fixed footer */}
</View>
If your root view doesn't have flex: 1, it is only as tall as its content — the common cause of
"my background colour only covers half the screen" and "my list doesn't scroll".
Multiple siblings with flex share space proportionally: flex: 2 and flex: 1 split remaining
space 2:1.
flex in React Native is not the CSS shorthand. A positive number n behaves like
flexGrow: n, flexShrink: 1, flexBasis: 0. If you need the CSS-style independent control, use
flexGrow, flexShrink and flexBasis directly.
Axes, justify and align¶
flexDirectionsets the main axis (columndefault, orrow).justifyContentdistributes children along the main axis:flex-start,center,flex-end,space-between,space-around,space-evenly.alignItemspositions children on the cross axis:stretch(default),flex-start,center,flex-end,baseline.alignSelfoverridesalignItemsfor one child.gap,rowGap,columnGapput space between children — much cleaner than margins on every child.flexWrap: 'wrap'lets a row wrap onto new lines (chips, tags, a photo grid).
Because the default alignItems is stretch, children in a column are full width. Set
alignItems: 'center' or alignSelf: 'flex-start' when you want a child to hug its content —
otherwise a "badge" stretches across the whole screen.
A row that doesn't overflow¶
The classic list-row layout — avatar, title that can be long, and a trailing value:
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, padding: 16 }}>
<View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: '#c7d2fe' }} />
<View style={{ flex: 1 }}>
<Text numberOfLines={1} style={{ fontSize: 16, fontWeight: '600' }}>
Read 20 pages of a book that has a remarkably long title
</Text>
<Text style={{ color: '#64748b' }}>Daily · 7:00 PM</Text>
</View>
<Text style={{ fontWeight: '700' }}>12🔥</Text>
</View>
The middle column's flex: 1 is what makes this work: it takes whatever width remains after the
fixed avatar and the trailing value, and numberOfLines={1} truncates inside that width. Remove
flex: 1 and the text pushes the streak off the screen, because nothing is allowed to shrink.
Absolute positioning¶
position: 'absolute' takes a view out of the flex flow and positions it relative to its parent
(every view is relative by default, so the parent is always the containing block):
<View>
<Image source={{ uri }} style={{ width: 64, height: 64, borderRadius: 32 }} />
<View style={{
position: 'absolute', right: -2, bottom: -2,
width: 18, height: 18, borderRadius: 9,
backgroundColor: '#22c55e', borderWidth: 2, borderColor: 'white',
}} />
</View>
Use it for badges, overlays and floating action buttons. Don't use it for general layout; it doesn't adapt to different screen sizes or font sizes.
Worked example: a full screen layout¶
import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
const habits = ['Drink water', 'Stretch 10 min', 'Read 20 pages', 'No phone after 10pm'];
export default function TodayScreen() {
return (
<View style={styles.screen}>
<View style={styles.header}>
<Text style={styles.title}>Today</Text>
<View style={styles.progressPill}>
<Text style={styles.progressText}>2 / 4</Text>
</View>
</View>
<ScrollView style={styles.body} contentContainerStyle={styles.bodyContent}>
<View style={styles.chips}>
{['Morning', 'Health', 'Focus', 'Evening', 'Weekend'].map((c) => (
<Text key={c} style={styles.chip}>{c}</Text>
))}
</View>
{habits.map((h) => (
<View key={h} style={styles.row}>
<Text style={styles.rowText} numberOfLines={1}>{h}</Text>
<View style={styles.check} />
</View>
))}
</ScrollView>
<View style={styles.footer}>
<Pressable style={styles.addButton} accessibilityRole="button">
<Text style={styles.addText}>Add habit</Text>
</Pressable>
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, backgroundColor: '#f8fafc' },
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingTop: 56, paddingBottom: 12 },
title: { fontSize: 28, fontWeight: '800' },
progressPill: { backgroundColor: '#e0e7ff', borderRadius: 999, paddingHorizontal: 12, paddingVertical: 4 },
progressText: { color: '#3730a3', fontWeight: '700' },
body: { flex: 1 },
bodyContent: { padding: 16, gap: 10 },
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 8 },
chip: { backgroundColor: 'white', borderRadius: 999, paddingHorizontal: 12, paddingVertical: 6, overflow: 'hidden' },
row: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'white', borderRadius: 12, padding: 16, gap: 12 },
rowText: { flex: 1, fontSize: 16 },
check: { width: 24, height: 24, borderRadius: 12, borderWidth: 2, borderColor: '#cbd5e1' },
footer: { padding: 16, paddingBottom: 32, backgroundColor: 'white' },
addButton: { backgroundColor: '#4f46e5', borderRadius: 12, paddingVertical: 14, alignItems: 'center' },
addText: { color: 'white', fontSize: 16, fontWeight: '700' },
});
The paddingTop: 56 and paddingBottom: 32 are stand-ins for the notch and the home indicator. They
are wrong on half the devices out there; lesson 8 replaces them with real safe-area insets.
How It Actually Works¶
React Native does not use a browser's layout engine. It uses Yoga, a cross-platform C++ implementation of a Flexbox subset, maintained by Meta. In the New Architecture, every host component has a C++ shadow node, and each shadow node owns a Yoga node with its layout styles.
When props change, Fabric clones the affected shadow nodes, Yoga recomputes layout for the dirty
part of the tree, and the result — an absolute x, y, width, height for every node — is
compared with the previous frame. Only views whose frames or props actually changed receive native
mutations. Layout runs off the UI thread, and measuring text calls into the platform's text system
so Yoga knows how tall a paragraph is at a given width.
This is why there's no CSS Grid or floats: Yoga implements what React Native needs, consistently on
every platform. It's also why the defaults differ — Yoga's defaults were chosen for mobile
(column, no shrinking) rather than for web compatibility. (React Native for Web, by contrast, maps
these styles back onto browser CSS.)
Common mistakes¶
- Missing
flex: 1on the root or on aScrollView's parent — content collapses or won't scroll. - Text pushing siblings off-screen in a row — give the text's container
flex: 1(orflexShrink: 1). - A child stretching full-width unexpectedly — default
alignItems: 'stretch'; usealignSelf. justifyContent: 'center'on aScrollView'sstyle— it must go oncontentContainerStyle, and centring scrollable content usually also needsflexGrow: 1there.- Hard-coded paddings for notches (as in this lesson's example) left in production.
Exercise¶
- Build a calculator keypad: a display area that takes the top third (
flex: 1) and a 4×5 grid of buttons in the bottom two thirds (flex: 2), using rows withflex: 1buttons andgap. - Make the "0" button span two columns using
flex: 2within its row. - Add a small red notification badge, absolutely positioned on the top-right corner of one button.
- Rotate your phone to landscape (enable rotation in
app.jsonwith"orientation": "default") and check the grid still fits — no fixed heights allowed.