Building Custom Layouts in Compose: Beyond Row and Column

By Engineering Team • Published: 2025-12-09 • Updated: 2025-12-0915 min read

Custom LayoutArchitectureAdvancedUI Components

When Built-in Layouts Are Not Enough

Row, Column, and Box cover 90% of layout needs. But sometimes you need pixel-perfect control: overlapping elements with specific offsets, flow layouts that wrap content, or performance-critical layouts that minimize measurement passes.

Custom layouts in Compose are surprisingly approachable. The Layout composable gives you direct access to measurement and placement, while maintaining the declarative paradigm.

The Layout Composable Basics

Every custom layout starts with the Layout composable. It takes content and a MeasurePolicy:

@Composable
fun SimpleCustomLayout(
    modifier: Modifier = Modifier,
    content: @Composable () -> Unit
) {
    Layout(
        content = content,
        modifier = modifier
    ) { measurables, constraints ->
        // 1. Measure children
        val placeables = measurables.map { measurable ->
            measurable.measure(constraints)
        }
        
        // 2. Decide layout size
        val width = placeables.maxOf { it.width }
        val height = placeables.sumOf { it.height }
        
        // 3. Place children
        layout(width, height) {
            var yPosition = 0
            placeables.forEach { placeable ->
                placeable.placeRelative(x = 0, y = yPosition)
                yPosition += placeable.height
            }
        }
    }
}

Real Example: Flow Layout

A flow layout wraps children to the next row when they exceed available width. This is perfect for tag clouds or chip groups:

@Composable
fun FlowRow(
    modifier: Modifier = Modifier,
    horizontalSpacing: Dp = 8.dp,
    verticalSpacing: Dp = 8.dp,
    content: @Composable () -> Unit
) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        val hSpacingPx = horizontalSpacing.roundToPx()
        val vSpacingPx = verticalSpacing.roundToPx()
        
        val placeables = measurables.map { it.measure(constraints) }
        
        var currentX = 0
        var currentY = 0
        var rowHeight = 0
        
        val positions = placeables.map { placeable ->
            if (currentX + placeable.width > constraints.maxWidth) {
                currentX = 0
                currentY += rowHeight + vSpacingPx
                rowHeight = 0
            }
            val position = IntOffset(currentX, currentY)
            currentX += placeable.width + hSpacingPx
            rowHeight = maxOf(rowHeight, placeable.height)
            position
        }
        
        layout(constraints.maxWidth, currentY + rowHeight) {
            placeables.forEachIndexed { index, placeable ->
                placeable.placeRelative(positions[index])
            }
        }
    }
}

Intrinsic Measurements

Sometimes parent layouts need to know child sizes before full measurement. Compose provides intrinsic measurements for this:

  • minIntrinsicWidth/Height: Minimum size needed to display content properly
  • maxIntrinsicWidth/Height: Size needed to display content without constraints
  • Use IntrinsicSize.Min or IntrinsicSize.Max modifiers to query these values
Note: Intrinsic measurements add an extra measurement pass. Use sparingly and only when necessary for correct layout behavior.
← Back to Blog