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.

Create a free account to save this page, take notes and track your progress. Sign in

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:

  • flexDirection defaults to column (things stack vertically).
  • Every View is a flex container; there's no display: block or grid.
  • flex: 1 means "take the remaining space"; it's how screens fill the device.
  • Spacing uses padding, margin and gap in 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.

Compact numbers and theme selectionTry it yourself
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: #ffffff

Common mistakes

  • Expecting rows by default. Add flexDirection: "row" for side-by-side layouts.
  • Fixed widths everywhere. Use flex, percentages and gap so 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.create and stable style objects avoid needless work.

Key points

  • Flexbox everywhere, defaulting to columns; flex: 1 fills space.
  • justifyContent (main axis) and alignItems (cross axis) do most layout work.
  • Wrap screens in SafeAreaView; respect useColorScheme().
  • 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.

Was this page helpful?