FlowRow is the chip group you kept writing a custom Layout for
FlowRow and FlowColumn wrap children onto new lines when they run out of room. They replace the custom Layout most codebases wrote for chip groups, and the overflow API handles the 'and 4 more' case.

Day 26 of 100. A Row puts everything on one line and lets the overflow fall off the
edge. Sometimes you want the overflow to wrap. That used to mean writing a custom
Layout; it doesn't any more.
The symptom
A tag list that runs off the screen:
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
tags.forEach { tag -> Chip(tag) }
}
Four tags fit. The fifth is clipped, the sixth doesn't render at all, and nothing warns you. On a phone in landscape it looks fine; in portrait with a long tag it doesn't.
Why the obvious fix fails
The obvious fix is to make it scroll:
Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { … }
Now nothing is clipped, and nothing is visible either — the tags past the fold require a horizontal swipe that most users won't discover, on a screen that scrolls vertically. A horizontally scrolling strip inside a vertically scrolling page is also a gesture conflict you have to think about.
The other obvious fix is chunking, as in Day 24:
tags.chunked(3).forEach { row -> Row { row.forEach { Chip(it) } } }
Three per line regardless of how wide they are, so a row of short tags wastes half the screen and a row of long ones still overflows. The chunk size is a guess about rendered width, made before anything has been measured.

The actual mechanism
FlowRow places children along the main axis and starts a new line when the next child
wouldn't fit:
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
tags.forEach { tag -> Chip(tag) }
}
Each chip is measured at its natural width, the layout tracks how much room is left on the current line, and anything that doesn't fit starts a new one. Long tags take more room, short ones pack in — because the decision is made after measuring rather than before.
Note both arrangements are set. horizontalArrangement spaces items within a line;
verticalArrangement spaces the lines themselves. Setting only the first gives you
correctly-spaced chips in lines that touch, which is the most common way this looks
subtly wrong.
FlowColumn is the same thing rotated: fills a column downward, starts a new column to
the side when it runs out of height.
Both were experimental for a long while and are stable in current Compose Foundation —
if your IDE demands @OptIn(ExperimentalLayoutApi::class), your Foundation version
predates stabilisation and the annotation is harmless.
Weights inside a flow
FlowRow supports weight per item, and it means something slightly different from
Row: the weight divides the space on that item's line, after line-breaking is
decided.
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Chip("kotlin", Modifier.weight(1f))
Chip("compose", Modifier.weight(1f))
Chip("a much longer tag name", Modifier.weight(1f))
}
Items still wrap by natural size, then stretch to fill their line. That's how you get a tag grid with flush edges rather than a ragged right margin.
fillMaxLineWidth() is the shortcut for "this one takes a whole line to itself", which
is how a section header lives inside a flow without a separate container.
Overflow: the "+4 more" case
The parameter that turns this from a layout into a component is overflow:
FlowRow(
maxLines = 2,
overflow = FlowRowOverflow.expandIndicator {
Chip("+${totalTags - shownItemCount} more", onClick = { expanded = true })
},
) {
tags.forEach { Chip(it) }
}
Two lines of tags, then an indicator that knows how many were hidden. shownItemCount
is provided by the overflow scope, so the count is derived from what actually fit rather
than from an estimate.
There's an expandOrCollapseIndicator variant that handles both directions, which is
the whole "show more / show less" interaction with no state of your own beyond the
expanded flag.
Before this existed, that feature meant measuring text yourself to guess how many chips fit — a calculation that was wrong on a different font scale, and a good example of why "just write a custom Layout" is more expensive than it first looks.
When it is still the wrong tool
FlowRow measures every child, including ones that will never be visible. With
maxLines = 2 and four hundred tags, you measure four hundred chips to display seven.
It is not lazy, and there is no lazy flow layout.
For a genuinely large collection, use a LazyVerticalStaggeredGrid or bound the input
before the layout sees it:
FlowRow(maxLines = 2, overflow = …) {
tags.take(30).forEach { Chip(it) } // 30 is plenty to fill two lines
}
Uncomfortable but honest: the layout has no way to know a chip won't fit without measuring it, so the caller has to draw the line.
Alignment within a line
Lines in a FlowRow can have different heights — a chip with an icon next to a plain
text chip — and itemVerticalAlignment decides how the shorter ones sit:
FlowRow(
itemVerticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) { … }
The default is Alignment.Top, which is rarely what a chip group wants — mixed-height
chips end up hanging from the line's top edge with a gap underneath. It's a one-line fix
that's easy to miss because it only shows up once the content is heterogeneous, which is
usually after the layout was already reviewed.
Arrangement.Center on the horizontal axis is worth knowing too: it centres each line
independently, so a final line with two chips sits centred under a full line rather than
left-aligned. Whether that looks right depends on the design, but it's the difference
between a tag cloud and a tag list.
How to prove it
The wrapping behaviour is a preview question, and worth keeping as a permanent one:
@Preview(widthDp = 320) @Preview(widthDp = 480) @Preview(widthDp = 720)
@Composable fun TagsAcrossWidths() = TagRow(sampleTags)
Three widths, three different line breaks, no conditional code. If the breaks don't
change, you're in a Row.
The font-scale case is the one that catches shipped code, and it's one more annotation:
@Preview(fontScale = 2f, widthDp = 360)
@Composable fun TagsLargeText() = TagRow(sampleTags)
At 2× font scale a chunked layout overflows and a flow layout re-wraps. Since font scale is a user accessibility setting rather than a device property, this is the preview that turns "works on my phone" into "works".
What this generalizes to
The idea is let the layout decide after measuring, not the caller before. Chunking by three is a prediction about rendered width made with no information; flow layout is the same decision made with the measurement in hand.
CSS flex-wrap is the same mechanism with the same trade — wrapping requires measuring everything, so it is not lazy, in any framework that offers it. The rule that transfers: wrap when the collection is small and the widths vary, virtualise when the collection is large and the widths are uniform. Reaching for the wrong one is what makes tag lists either clipped or slow.
Tomorrow, Day 27: non-lazy grids, and when a plain Column of Rows is genuinely the
right answer.
Day 26 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Flow layouts.