Row vs Column vs Box in Jetpack Compose

Quick answer: Use Row for side-by-side children, Column for vertically stacked children, and Box when children overlap. Configure the container’s arrangement and alignment first; give an individual child a scoped align modifier only when it needs an exception.

Row, Column, and Box are the three layout building blocks you will use constantly in Jetpack Compose. They are deliberately small APIs, but choosing the right one makes a screen easier to read, adapt, and maintain.

The official Compose layout basics describe the distinction simply: Row places children horizontally, Column places them vertically, and Box stacks them. This guide turns that rule into practical decisions you can use in real UI.

Choose the layout by the visual relationship

You want to showUseCommon examples
Items beside one anotherRowicon and label, profile header, action buttons
Items one below anotherColumnsettings form, article card, screen sections
Items on top of one anotherBoximage badge, loading layer, floating button over content

Think in axes:

  • A Row has a horizontal main axis and a vertical cross axis.
  • A Column has a vertical main axis and a horizontal cross axis.
  • A Box has no flow axis for its children; it positions each child in the same available space, so children can overlap.

For a refresher on how these pieces are composed into UI, see what Jetpack Compose is.

Use Row for horizontal content

Row is the natural fit when reading order is left-to-right: an avatar next to profile information, or a title next to an action.

@Composable
fun ProfileHeader(
    name: String,
    handle: String,
    onMoreClick: () -> Unit,
) {
    Row(
        modifier = Modifier.fillMaxWidth(),
        verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.spacedBy(12.dp),
    ) {
        Box(
            modifier = Modifier
                .size(48.dp)
                .clip(CircleShape)
                .background(MaterialTheme.colorScheme.primary),
        )

        Column(modifier = Modifier.weight(1f)) {
            Text(text = name, style = MaterialTheme.typography.titleMedium)
            Text(text = handle, style = MaterialTheme.typography.bodyMedium)
        }

        IconButton(onClick = onMoreClick) {
            Icon(Icons.Outlined.MoreVert, contentDescription = "More options")
        }
    }
}

horizontalArrangement controls distribution along the Row’s main axis. verticalAlignment controls the cross axis. Here, the name block receives weight(1f), so it consumes the remaining horizontal space and pushes the action to the end.

weight is a scope-specific modifier: it works inside Row and Column, not anywhere in the UI tree. In a Row, it allocates width; in a Column, it allocates height. The Modifier documentation explains this parent-data behavior in more detail.

Use Column for vertical sections

Choose Column when a user should scan content from top to bottom. A settings section, login form, and empty state are all common examples.

@Composable
fun EmptyDownloads(
    onBrowseClick: () -> Unit,
) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Icon(
            imageVector = Icons.Outlined.Download,
            contentDescription = null,
            modifier = Modifier.size(48.dp),
        )
        Spacer(Modifier.height(16.dp))
        Text("No downloads yet", style = MaterialTheme.typography.titleLarge)
        Spacer(Modifier.height(8.dp))
        Text("Save an item to read it later.")
        Spacer(Modifier.height(20.dp))
        Button(onClick = onBrowseClick) {
            Text("Browse items")
        }
    }
}

For a Column, the names are reversed from Row: verticalArrangement controls the main axis and horizontalAlignment controls the cross axis. Use Arrangement.spacedBy(16.dp) when every adjacent item should have the same gap; it is often clearer than adding a Spacer after each child.

Use Box for layers and overlays

Box is for a different relationship: several elements occupy the same region. It is ideal for a badge on an image, a progress indicator above content, or a button anchored over a map.

@Composable
fun PhotoWithBadge(
    photo: Painter,
    unreadCount: Int,
) {
    Box(modifier = Modifier.size(160.dp)) {
        Image(
            painter = photo,
            contentDescription = "Trip photo",
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize(),
        )

        if (unreadCount > 0) {
            Badge(
                modifier = Modifier
                    .align(Alignment.TopEnd)
                    .padding(8.dp),
            ) {
                Text(unreadCount.toString())
            }
        }
    }
}

Alignment.TopEnd is a BoxScope modifier, so it is available to direct children of a Box. You can also set a default position for all children with contentAlignment:

Box(
    modifier = Modifier.size(120.dp),
    contentAlignment = Alignment.Center,
) {
    CircularProgressIndicator()
}

matchParentSize() versus fillMaxSize() in a Box

These modifiers can look similar but serve different purposes. fillMaxSize() asks a child to fill the constraints given by its parent, which can make that child affect the size of the Box. matchParentSize() instead waits for the Box size established by its other children, then matches it.

Use matchParentSize() for a background, scrim, or selection layer that must follow content without deciding the container’s size:

Box {
    Box(
        Modifier
            .matchParentSize()
            .background(Color.Black.copy(alpha = 0.2f)),
    )
    Text("Content that defines the Box size", Modifier.padding(16.dp))
}

This behavior is documented on the official BoxScope.matchParentSize reference.

Arrangement, alignment, and child exceptions

Use the container parameters when the rule applies to most children. Use a child’s scoped align modifier only for the exception.

Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.spacedBy(12.dp),
) {
    Text("Centered title")
    Text("Centered description")
    Button(
        onClick = {},
        modifier = Modifier.align(Alignment.End),
    ) {
        Text("Continue")
    }
}

The title and description inherit the centered cross-axis alignment. The button deliberately opts into Alignment.End. This is easier to understand than placing every child in its own wrapper just to change its position.

Common layout mistakes

Using a Column for a long, scrollable feed

A regular Column creates its children as part of the composition. For a long or unbounded list, use a lazy layout so visible items are composed as needed. Start with the LazyColumn guide when building a feed or results list.

Reaching for nested boxes when a row or column expresses the intent

Nested layouts are normal in Compose, but the outermost layout should communicate the primary relationship. A profile header is usually a Row containing a Column, not a stack of Box containers with manually positioned content. Compose is designed to handle nested layouts efficiently; prefer clarity and measure real performance before optimizing a readable hierarchy away.

Using Spacer everywhere

Spacer is useful for one-off gaps and flexible empty space. When all children need the same gap, Arrangement.spacedBy() states the intent once and avoids repeated spacing code.

Forgetting to test layout variants

The correct arrangement at a normal font size can clip or crowd at a larger scale, in dark theme, or with longer translated text. Create small component previews for your Row, Column, and Box layouts; Compose Preview makes those variants quick to inspect.

A practical decision checklist

Before adding a layout container, ask:

  1. Do the children belong beside, below, or on top of each other?
  2. Which axis should distribute space?
  3. Does every child follow the same alignment rule?
  4. Is this a small static group or a long list that needs a lazy layout?
  5. Can the composable be previewed with short, long, and empty content?

The answers usually point directly to Row, Column, or Box. Start with the simplest container that expresses the visual relationship, then add modifiers only when they solve a real layout requirement. The next layer of control is understanding Compose modifiers, including how modifier order changes a component’s behavior.

FAQ

Can I put a Column inside a Row?

Yes. This is one of the most common Compose patterns: use the Row for the broad horizontal structure and a nested Column for related text or actions that should stack vertically.

Should I use Row or Box for an icon beside text?

Use Row. The icon and text are separate, side-by-side siblings. Use Box only when one element should overlay another, such as a notification dot on an icon.

Does weight() work in Box?

No. weight() belongs to RowScope and ColumnScope, where there is a main axis along which remaining space can be shared. Position children in a Box with contentAlignment or Modifier.align() instead.