Mobile Apps with React Native and Expo · Foundations: Screens, Styles, Lists and Navigation
Layout and Styling with Flexbox
Every React Native screen is laid out with Flexbox, but with mobile defaults: columns, not rows. Build a profile card, learn flex, alignment and spacing, and handle the notch and dark mode.
What you'll build
A profile card: an avatar on the left, name and role stacked on the right, and a row of three stats underneath, laid out correctly on any screen size and in dark mode.
The idea
React Native uses Flexbox for all layout, with a few mobile-friendly differences from the web:
flexDirectiondefaults tocolumn(things stack vertically).- Every
Viewis a flex container; there's nodisplay: blockorgrid. flex: 1means "take the remaining space"; it's how screens fill the device.- Spacing uses
padding,marginandgapin density-independent pixels.
The four properties you'll use constantly:
| Property | Controls | Common values |
|---|---|---|
flexDirection |
main axis | "column" (default), "row" |
justifyContent |
along the main axis | "flex-start", "center", "space-between" |
alignItems |
across the main axis | "stretch" (default), "center", "flex-start" |
flex |
share of free space | 1 to fill |
Phones have safe areas (notches, home indicators, status bars). Wrap screens in SafeAreaView from react-native-safe-area-context so content isn't hidden. For dark mode, read useColorScheme() and pick colours from a theme object.
The code
import { StyleSheet, Text, View, useColorScheme } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { Image } from "expo-image";
const themes = {
light: { bg: "#ffffff", card: "#f4f4f5", text: "#18181b", muted: "#52525b" },
dark: { bg: "#09090b", card: "#18181b", text: "#fafafa", muted: "#a1a1aa" },
};
export default function Profile() {
const t = themes[useColorScheme() === "dark" ? "dark" : "light"];
return (
<SafeAreaView style={[styles.screen, { backgroundColor: t.bg }]}>
<View style={[styles.card, { backgroundColor: t.card }]}>
<View style={styles.header}>
<Image source={{ uri: "https://example.com/ana.jpg" }} style={styles.avatar} />
<View style={{ flex: 1 }}>
<Text style={[styles.name, { color: t.text }]}>Ana Silva</Text>
<Text style={{ color: t.muted }}>Mobile developer</Text>
</View>
</View>
<View style={styles.stats}>
{[["Posts", 128], ["Followers", 2400], ["Following", 180]].map(([label, value]) => (
<View key={label} style={styles.stat}>
<Text style={[styles.statValue, { color: t.text }]}>{value}</Text>
<Text style={{ color: t.muted }}>{label}</Text>
</View>
))}
</View>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16 },
card: { borderRadius: 16, padding: 16, gap: 16 },
header: { flexDirection: "row", alignItems: "center", gap: 12 },
avatar: { width: 56, height: 56, borderRadius: 28 },
name: { fontSize: 18, fontWeight: "600" },
stats: { flexDirection: "row", justifyContent: "space-between" },
stat: { alignItems: "center", flex: 1 },
statValue: { fontSize: 18, fontWeight: "700" },
});
Try the logic
Numbers like "2400 followers" should read "2.4K" on a small card. Formatting helpers like this belong in plain TypeScript, where they're easy to check.
function compact(n: number): string {
if (n < 1000) return String(n);
if (n < 1_000_000) return `${(n / 1000).toFixed(n < 10_000 ? 1 : 0).replace(/\.0$/, "")}K`;
return `${(n / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
}
type Scheme = "light" | "dark" | null | undefined; // useColorScheme() can return null
const themes = {
light: { bg: "#ffffff", text: "#18181b" },
dark: { bg: "#09090b", text: "#fafafa" },
};
const themeFor = (scheme: Scheme) => themes[scheme === "dark" ? "dark" : "light"];
for (const n of [128, 999, 2400, 12_345, 1_000_000, 3_250_000]) {
console.log(String(n).padStart(9), "->", compact(n));
}
console.log("dark text colour:", themeFor("dark").text);
console.log("unknown scheme falls back to:", themeFor(null).bg);
Output
128 -> 128
999 -> 999
2400 -> 2.4K
12345 -> 12K
1000000 -> 1M
3250000 -> 3.3M
dark text colour: #fafafa
unknown scheme falls back to: #ffffffCommon mistakes
- Expecting rows by default. Add
flexDirection: "row"for side-by-side layouts. - Fixed widths everywhere. Use
flex, percentages andgapso layouts adapt to small and large phones and tablets. - Ignoring safe areas. Content slides under the notch or the home indicator.
- Hard-coded colours. Dark mode users get white screens at night; use a theme.
- Inline style objects in long lists.
StyleSheet.createand stable style objects avoid needless work.
Key points
- Flexbox everywhere, defaulting to columns;
flex: 1fills space. justifyContent(main axis) andalignItems(cross axis) do most layout work.- Wrap screens in
SafeAreaView; respectuseColorScheme(). - Keep formatting and theme logic in plain functions.
Further reading: Layout with Flexbox, Color themes in Expo.
Practice questions
What is the default flexDirection in React Native, and how does that differ from the web?
Answer
column: children stack vertically. On the web, a flex container defaults to row.
Why wrap screens in a SafeAreaView?
Answer
Modern phones have notches, rounded corners, status bars and home indicators. SafeAreaView (from react-native-safe-area-context) adds padding so content isn't drawn underneath them.
Open in the app to mark lessons complete, take notes and track your progress.