Flexbox Layout

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Every style prop touched so far — on App.tsx in Getting Started, on PlaceCard in Core Components and React Fundamentals and TypeScript in React Native — has quietly relied on flex, flexDirection, alignItems, and justifyContent without explaining them. This page covers that layout system in full: Yoga, the engine that computes it; every layout property it understands; safe areas and Android’s now-mandatory edge-to-edge display; a small catalogue of layout patterns Places reuses across screens; and the Places home screen — header, list, and floating action button — laid out with exactly these properties. Visual styling itself — colors, typography, and the theme object this page’s examples consume via useTheme() — is Styling's subject, not this page’s; this page is about where things sit, not what they look like.

Yoga: React Native’s Layout Engine

Yoga is Meta’s own C++ implementation of the Flexbox specification, compiled into every React Native app and invoked from both Fabric (rendering) and Codegen-produced native code — it is not the browser’s CSS layout engine, and it is not WebKit’s or Blink’s Flexbox implementation reused, even though it targets the same specification and, for the most part, the same property names. That shared vocabulary is precisely what makes one property’s default behave differently enough to matter:

flexDirection defaults to 'column' in React Native, not 'row'. This is the single biggest difference from web CSS Flexbox, whose own default is row. Every unstyled View in Places stacks its children top-to-bottom unless flexDirection: 'row' is set explicitly — code ported from a web Flexbox layout that never set flexDirection will render its children in the wrong direction until this is corrected.

With that one default flipped, the rest of the property set works exactly as it does in browser Flexbox:

Property What it controls

flexDirection

Which axis is the main axis: 'column' (default, top-to-bottom), 'row', 'column-reverse', 'row-reverse'.

justifyContent

Distributes children along the main axis: flex-start (default), flex-end, center, space-between, space-around, space-evenly.

alignItems

Aligns children along the cross axis (the axis perpendicular to flexDirection), on the container: stretch (default), flex-start, flex-end, center, baseline.

alignSelf

The same set of values as alignItems, overriding the container’s alignItems for one child only.

alignContent

Distributes rows of children along the cross axis when flexWrap: 'wrap' produces more than one line —  meaningless with a single line, the wrapped-multi-line analogue of justifyContent.

flex

Shorthand most Places code uses directly: a single non-negative number sets flexGrow to that number and flexShrink to 1, so { flex: 1 } means "grow to fill remaining space, shrink if needed" — the same everyday meaning flex: 1 has on the web.

flexGrow / flexShrink / flexBasis

The three properties flex is shorthand for, settable individually when a child needs to grow but never shrink, or start from a size other than its content size — flexBasis sets that starting main-axis size before flexGrow/flexShrink redistribute any remaining or overflowing space.

flexWrap

'nowrap' (default — children overflow or shrink to fit one line) or 'wrap' (children that no longer fit wrap onto additional lines along the cross axis) — the Places grid catalogue entry below depends on this.

gap, rowGap, columnGap

Fixed space between children along the main axis (gap/columnGap in a row) and cross axis (gap/rowGap in a row) — added since React Native 0.71, and now the preferred way to space children evenly without a margin on every child except the last.

position: 'absolute'

Removes a child from Yoga’s normal flex flow entirely and positions it relative to its nearest positioned ancestor, using top/right/bottom/left, or the inset shorthand that sets all four at once (inset: 0 fills the ancestor exactly) — Places's floating action button below relies on this.

Percentage dimensions, aspectRatio

width/height/top/etc. accept a percentage string ('50%') resolved against the parent’s own resolved size; aspectRatio constrains width:height (a number, e.g. 16 / 9) so only one dimension needs to be set explicitly and the other is derived.

overflow

'visible' (default) lets a child paint outside its parent’s bounds; 'hidden' clips it there; 'scroll' behaves like 'hidden' but additionally allows programmatic scrolling of the clipped content (rarely used directly — ScrollView/FlatList manage their own scrolling instead).

zIndex

Paint order among siblings that would otherwise overlap — higher values paint on top, independent of JSX order — used below to keep Places's floating action button above the list beneath it.

Diagram of Yoga’s main axis and cross axis under flexDirection: column versus flexDirection: row

None of this is negotiated per platform: Yoga runs identically on iOS and Android (and, via the same engine, inside the New Architecture’s C++ layer described on Architecture Under the Hood), so a layout that is correct on one platform is, barring a genuinely platform-specific measurement quirk, correct on the other without any Platform.OS branch.

Safe Areas and Edge-to-Edge Display

A layout that fills its View correctly can still end up behind a notch, a status bar, a home indicator, or a gesture-navigation bar, because Yoga lays out within the space it is given — it has no built-in notion of "the part of the screen the OS reserves for itself." Two tools close that gap, and only one of them is still the right default:

  • SafeAreaView — exported directly from react-native — insets its children away from the notch and status bar on iOS only; historically it was a no-op on Android, which is why it was never a reliable cross-platform solution even before the change below.

  • react-native-safe-area-context's SafeAreaProvider (wrapped once, near the root, alongside Places's own ThemeProvider from Styling) and useSafeAreaInsets() — a Hook returning { top, right, bottom, left } in points — is the actually-recommended approach today: it resolves correctly on both platforms, updates on rotation and on-screen-keyboard changes, and lets a screen apply exactly the insets it needs (often just paddingTop: insets.top on a header) rather than wrapping an entire screen in an opaque SafeAreaView box.

React Native 0.86 made edge-to-edge display mandatory on Android 15 and later — the OS itself no longer letterboxes an app’s content away from the status bar and navigation bar by default the way earlier Android versions did. An app that never accounted for insets used to get away with it because the OS drew a safe margin for it; on Android 15+ that margin is gone, and content that does not call useSafeAreaInsets() (or otherwise apply its own top/bottom padding) now renders under the status bar and the gesture/navigation bar. Places treats react-native-safe-area-context as required infrastructure from this page onward specifically because of this change, not only as a nicety for iOS notches.

Responsive re-layout on rotation or window resize — a tablet turned sideways, a foldable unfolded, a window resized in Android’s multi-window mode — is useWindowDimensions()’s job, already introduced on Styling: unlike a one-shot `Dimensions.get('window') call, useWindowDimensions() is a Hook that re-renders the component with fresh { width, height } whenever either changes, which is exactly what the responsive Places grid later on this page depends on.

A Layout Catalogue

A handful of flex-prop combinations cover most of the shapes a Places screen actually needs. Each snippet below composes only View, flex, and the properties from the table above — no third-party grid library.

A three-column row, each column sharing the remaining width equally:

<View style={{ flexDirection: 'row', gap: theme.spacing.md }}>
  <View style={{ flex: 1 }}><Text>Trips</Text></View>
  <View style={{ flex: 1 }}><Text>Places</Text></View>
  <View style={{ flex: 1 }}><Text>Favourites</Text></View>
</View>

A flexible row that spreads its children across the full width, useful for a card’s label-and-value pair or a screen’s title-and-action header:

<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
  <Text style={{ fontWeight: '600' }}>Category</Text>
  <Text>{place.category}</Text>
</View>

A flexible grid, wrapping fixed-fraction items onto additional rows once they no longer fit:

<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.sm }}>
  {categories.map((category) => (
    <View key={category} style={{ width: '31%' }}>
      <Text>{category}</Text>
    </View>
  ))}
</View>

A rows-and-columns grid, built as a column of rows rather than as a single wrapping row, when items must line up in fixed columns instead of merely filling whatever space is left on each line:

<View style={{ gap: theme.spacing.sm }}>
  {chunk(categories, 3).map((row, rowIndex) => (
    <View key={rowIndex} style={{ flexDirection: 'row', gap: theme.spacing.sm }}>
      {row.map((category) => (
        <View key={category} style={{ flex: 1 }}>
          <Text>{category}</Text>
        </View>
      ))}
    </View>
  ))}
</View>
Diagram comparing a three-column flex row to a wrapping percentage-width grid

The Places Home Screen Layout

PlacesListScreen — Places's home screen, first named on Getting Started — composes exactly three regions with the properties above: a header inset from the top safe area, a FlatList of PlaceCard`s filling the remaining space, and a floating action button pinned to the bottom-right corner via `position: 'absolute'. How FlatList itself virtualizes and scrolls that data is Lists and Scrolling's subject; here it is only ever the flex child that fills the screen between the header and the button:

// src/features/places/screens/PlacesListScreen.tsx
import { FlatList, Pressable, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTheme } from '../../../theme/ThemeProvider';
import { usePlaces } from '../hooks/usePlaces';
import { PlaceCard } from '../components/PlaceCard';
import type { Place } from '../types';

export function PlacesListScreen({ navigation }: PlacesListScreenProps) {
  const theme = useTheme();
  const insets = useSafeAreaInsets();
  const { data, fetchNextPage } = usePlaces();
  const places: Place[] = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <View style={{ flex: 1, backgroundColor: theme.colors.background }}>
      <View
        style={{
          paddingTop: insets.top + theme.spacing.md,
          paddingHorizontal: theme.spacing.md,
          paddingBottom: theme.spacing.sm,
        }}
      >
        <Text style={{ fontSize: 22, fontWeight: '700', color: theme.colors.text }}>Places</Text>
      </View>

      <FlatList
        style={{ flex: 1 }}
        contentContainerStyle={{ paddingHorizontal: theme.spacing.md, gap: theme.spacing.sm }}
        data={places}
        keyExtractor={(place) => place.id}
        onEndReached={() => fetchNextPage()}
        renderItem={({ item }) => (
          <PlaceCard
            place={item}
            onPress={(placeId) => navigation.navigate('PlaceDetails', { placeId })}
            onToggleFavourite={(placeId) => toggleFavourite(placeId)}
          />
        )}
      />

      <Pressable
        onPress={() => navigation.navigate('AddPlace')}
        style={{
          position: 'absolute',
          right: theme.spacing.lg,
          bottom: insets.bottom + theme.spacing.lg,
          width: 56,
          height: 56,
          borderRadius: 28,
          backgroundColor: theme.colors.primary,
          alignItems: 'center',
          justifyContent: 'center',
          zIndex: 10,
        }}
      >
        <Text style={{ fontSize: 26, color: theme.colors.onPrimary }}>+</Text>
      </Pressable>
    </View>
  );
}

Three things about this layout are worth calling out because they are easy to get wrong the first time:

  • The outer View is { flex: 1 } so the screen fills the space React Navigation’s stack hands it — without it, the FlatList beneath it (also flex: 1) would have nothing to grow into and would collapse to zero height.

  • Only the header adds insets.top; the FlatList itself needs no top inset because it sits below the header, and the floating action button adds insets.bottom instead, so it never sits under a gesture-navigation bar on Android 15+ edge-to-edge, or the home indicator on iOS.

  • The button’s zIndex: 10 is what keeps it painted above the list, since position: 'absolute' alone only removes it from flex flow — without an explicit zIndex, sibling paint order (the list, declared first in JSX, versus the button, declared second) already happens to put the button on top here, but relying on declaration order instead of zIndex is fragile the moment a screen’s JSX is reordered.

A Responsive Two-Column Grid for Tablets

useWindowDimensions() lets the same screen switch from a single-column list to a two-column grid once the window is wide enough to make a second column worthwhile — a tablet in portrait, or any window past a 768 logical-pixel breakpoint:

// src/features/places/screens/PlacesListScreen.tsx (responsive variant)
import { useWindowDimensions } from 'react-native';

const TABLET_BREAKPOINT = 768;

export function PlacesListScreen({ navigation }: PlacesListScreenProps) {
  const theme = useTheme();
  const { width } = useWindowDimensions();
  const isTablet = width >= TABLET_BREAKPOINT;
  const numColumns = isTablet ? 2 : 1;

  // ...usePlaces() and insets exactly as above...

  return (
    <View style={{ flex: 1, backgroundColor: theme.colors.background }}>
      {/* ...header exactly as above... */}
      <FlatList
        key={numColumns} // FlatList requires a new key when numColumns changes
        style={{ flex: 1 }}
        contentContainerStyle={{ paddingHorizontal: theme.spacing.md, gap: theme.spacing.sm }}
        columnWrapperStyle={isTablet ? { gap: theme.spacing.sm } : undefined}
        numColumns={numColumns}
        data={places}
        keyExtractor={(place) => place.id}
        renderItem={({ item }) => (
          <PlaceCard
            place={item}
            style={isTablet ? { flex: 1 } : undefined}
            onPress={(placeId) => navigation.navigate('PlaceDetails', { placeId })}
            onToggleFavourite={(placeId) => toggleFavourite(placeId)}
          />
        )}
      />
      {/* ...floating action button exactly as above... */}
    </View>
  );
}

numColumns is a FlatList prop, not a Yoga property — it tells FlatList to lay each row out as a flexDirection: 'row' group of numColumns items internally, which is why PlaceCard also needs { flex: 1 } (via the typed style override from TypeScript in React Native's fully-typed PlaceCard) once isTablet is true, so each card shares its row evenly rather than staying pinned to its single-column width. The key={numColumns} remount is required by FlatList itself: changing numColumns on an already-mounted list without changing its key throws, since the list’s internal cell layout is computed once per column count.