padding().background() and background().padding() are not the same picture
Modifier order changes rendered output because each element wraps the remainder of the chain. The outside-in reading rule resolves padding, background, clickable ripple bounds and clip behaviour in one go.

Day 20 of 100. Yesterday: a modifier is an ordered linked list. Today: why that order is the single most common source of "it looks wrong and I don't know why".
The symptom
Two chains, same elements, different output:
Box(Modifier.padding(16.dp).background(Color.Red).size(100.dp))
Box(Modifier.background(Color.Red).padding(16.dp).size(100.dp))
The first draws a red square with transparent space around it. The second draws a red square with red space around the content. Same three lines, reordered.
People learn this as "padding before background means outside padding" and memorise a handful of cases. The memorised version breaks the moment a fourth element appears.
Why the obvious mental model fails
The obvious model is that a modifier chain is a style sheet — a bag of properties where order is cosmetic. Under that model the two chains above should be identical, and every result becomes a surprise to be memorised individually.
The model that actually predicts the output is different, and it's simpler.

The actual mechanism
Each element wraps everything after it. The chain reads outside-in, exactly like nested boxes.
Modifier.padding(16.dp).background(Red).size(100.dp)
reads as: padding, containing a red background, containing a 100dp area. The padding is outermost, so the background starts inside it — a red square with transparent margin.
Modifier.background(Red).padding(16.dp).size(100.dp)
reads as: red background, containing padding, containing a 100dp area. The background is outermost, so it covers the padding too — the padded region is red.
Constraints flow the same direction, which is the mechanical version of the same fact.
padding(16.dp) reduces the constraints it passes inward by 32dp total; size(100.dp)
fixes them. So in the first chain the element is 100dp plus 32dp of padding = 132dp
overall; in the second, also 132dp, but painted red throughout.
One rule — outside-in — replaces every memorised case.
The four places it bites
Clickable and ripple bounds.
Modifier.padding(16.dp).clickable { } // padding is OUTSIDE: not clickable
Modifier.clickable { }.padding(16.dp) // clickable is OUTSIDE: padding is tappable
The second is almost always what you want — a larger touch target. The first produces a button whose visible padding does nothing, a classic "why do I have to tap exactly the text" bug.
Clip and background.
Modifier.background(Red).clip(CircleShape) // clips AFTER painting: still a square
Modifier.clip(CircleShape).background(Red) // clip is outside: red circle
Clip only affects what's drawn inside it. Anything painted further out is unaffected.
Size and padding.
Modifier.size(48.dp).padding(8.dp) // 48dp total, 32dp of content
Modifier.padding(8.dp).size(48.dp) // 64dp total, 48dp of content
Both are legitimate; they answer different questions. Icon buttons want the first (fixed touch target, inset icon). Cards usually want the second.
Border and padding.
Modifier.border(1.dp, Gray).padding(8.dp) // padding inside the border
Modifier.padding(8.dp).border(1.dp, Gray) // padding outside — border hugs content
The order that is almost always right
A default worth internalising, because it composes cleanly:
Modifier
.fillMaxWidth() // 1. size / layout
.padding(horizontal = 16.dp) // 2. outer spacing (margin-like)
.clip(RoundedCornerShape(12.dp)) // 3. shape
.background(Surface) // 4. paint, clipped by 3
.clickable { } // 5. interaction, covers all of the above
.padding(16.dp) // 6. inner spacing (padding-like)
Note padding appears twice, deliberately. Compose has no separate margin concept — the padding before the background is the margin; the padding after it is the padding. That's the whole distinction, and it's expressed by position rather than by two APIs.
Note also that clickable sits after background and clip, so the ripple is
clipped to the rounded shape and covers the painted area. Move it earlier and the
ripple spills into the margin.
The exception worth knowing
Not every modifier is order-sensitive. Semantics modifiers (testTag,
contentDescription) attach to the node rather than wrapping geometry, so their
position in the chain rarely changes anything visible.
And a few combinations are genuinely commutative — two padding calls sum, in either
order. But "rarely matters" is not "never matters", and the outside-in rule costs
nothing to apply, so it's better used everywhere than reasoned about case by case.
How to prove it
The fastest demonstration is a side-by-side preview:
@Preview
@Composable
fun OrderMatters() {
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Box(Modifier.padding(16.dp).background(Red).size(60.dp))
Box(Modifier.background(Red).padding(16.dp).size(60.dp))
Box(Modifier.background(Red).size(60.dp).padding(16.dp))
}
}
Three chains, three different squares, one screenshot. Keep it in the codebase as a reference preview — it answers the question faster than any explanation, including this one.
Layout Inspector confirms the geometry: each modifier that affects layout appears as its own node in the tree, nested in chain order. Seeing the chain rendered as literal nesting is what makes the rule click permanently.
Two orderings that look wrong and aren't
fillMaxSize() after a size().
Modifier.size(100.dp).fillMaxSize() // stays 100dp
size fixed min = max, so fillMaxSize fills a range with one value in it. Not a bug —
the outer element already answered the question. Reverse them and size wins instead,
because it's now the inner one. The general form: the innermost sizing modifier that
sets a fixed constraint is the one that decides.
padding before fillMaxWidth.
Modifier.padding(16.dp).fillMaxWidth() // fills the width MINUS 32dp
Modifier.fillMaxWidth().padding(16.dp) // fills full width, content inset by 16dp
Both produce a 16dp visual gap at each edge. They differ in what's underneath — in the first the element genuinely is narrower, so a background painted further in stops short of the screen edge. Pick based on whether the background should reach the edge.
A reading trick
When a chain misbehaves, read it aloud right-to-left as a sentence with "inside":
size(100)insidebackground(Red)insidepadding(16)
Whatever the sentence describes is what renders. It sounds trivial and it resolves essentially every case, because it's a literal restatement of the wrapping rule rather than a heuristic about it.
Where the rule stops applying
Two honest caveats, because "order always matters" over-promises.
Modifiers that only add information to the node — testTag, semantics,
contentDescription, zIndex — attach rather than wrap, so moving them along the chain
usually changes nothing observable. Placing them last by convention keeps chains
readable without implying the position is load-bearing.
And some elements genuinely commute. Two padding calls sum; two zIndex calls resolve
to the last one. Knowing which is which is less useful than applying outside-in
uniformly, because the uniform rule is never wrong and the exceptions never bite.
What this generalizes to
This is function composition with a visual result. a.b.c means a(b(c(x))), and
function composition has never been commutative — padding(background(x)) and
background(padding(x)) are different expressions in any language.
CSS made the opposite choice: properties are an unordered bag, and the cascade resolves conflicts by specificity rules that take years to internalise. Compose made order explicit and meaningful instead. It's one more thing to know, and one fewer thing to guess.
The trade shows up in tooling too. Because a chain is data, a linter can reason about
it — Compose's own lint catches clickable before padding and a handful of other
ordering mistakes — whereas a CSS cascade conflict can only be resolved by rendering the
page and looking.
Tomorrow, Day 21: constraints — how size, fillMax* and wrapContent actually
negotiate, and why fillMaxWidth() sometimes does nothing at all.
Day 20 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Order of modifiers.