Understanding Modifier Ordering in Jetpack Compose
This article was published in ProAndroidDev so you could also read it here
Okay, so modifier ordering in Compose. This thing has been driving me crazy for months.
Iβll be honest - I was that developer who just moved modifiers around until stuff looked right. Not proud of it, but hey, weβve all been there, right?
Then I found these two really good blog posts. One by MΓ‘rton Braun that says βmodifiers are applied last-to-first, inside-to-outsideβ and another by Marcin Moskala that basically says βno, modifiers are NOT applied from bottom to top.β
And Iβm sitting here likeβ¦ wait, what? These sound like theyβre saying opposite things.
So I spent some time trying to figure this out, and I think I finally got it. Maybe my way of thinking about it will help you too.
My Simple Rule: Just Read The Damn Thing
After all this confusion, I came up with the simplest possible way to think about it: Modifiers are applied as they are read. Top to bottom, from outside to inside.
Look at this:
fun SimpleExample(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(100.dp)
.background(Color.Blue)
) {
Text("I am a Box")
}
}

You read it as βa box with size 100dp and blue backgroundβ and thatβs exactly what you get. A 100Γ100dp blue box. No tricks, no magic.
This worked for me 90% of the time, and I was pretty happy with this understanding.
Then Padding Happened
The box example below really opened my eyes. Check this out:
Box(
modifier = Modifier
.size(150.dp)
.padding(20.dp)
.background(Color.Red)
.padding(20.dp)
.background(Color.Green)
.padding(20.dp)
.background(Color.Blue)
)
Reading top to bottom and applying from outside to inside: A Box of size 150dp then add padding, paint red, add more padding, paint green, add more padding, paint blue.
Each padding creates space, and each background fills whatever space is available at that moment.

Even if you add any other content composable like:
Box(
modifier = modifier.size(200.dp)
) {
Text(
modifier = Modifier
.padding(20.dp)
.background(Color.Red)
.padding(20.dp)
.background(Color.Green)
.padding(20.dp)
.background(Color.Blue),
text = "Some Text",
)
}
Thereβs a Box with dimensions 200Γ200, then spacing of 20dp is applied. Now a red background is applied (as weβre moving outward to inward), the color gets applied to the reduced area. Again padding of 20dp, again a background color, and so on. When all the modifiers have been applied, we can finally draw our Text.
Notice that I applied the modifier parameter before text as it helps me imagine that modifiers are applied before the content is drawn, since modifiers decide how much space the content can take.
βββββββββββββββββββββββββββββββββ
β Box (200dp) β
β βββββββββββββββββββββββββββ β
β β Red (padding 20dp) β β
β β βββββββββββββββββββββ β β
β β β Green (pad 20dp) β β β
β β β βββββββββββββββ β β β
β β β βBlue (20dp) β β β β
β β β β Some Text β β β β
β β β βββββββββββββββ β β β
β β βββββββββββββββββββββ β β
β βββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββ

This still fit my βread as you goβ model perfectly. I was feeling pretty confident.
When Everything Broke
Box(
modifier = Modifier
.size(200.dp)
.background(Color.Blue)
.size(100.dp)
)
My brain: β200dp size, blue background, then resize to 100dp. Should be a 100Γ100dp blue box.β
Reality: Nope. Still a 200Γ200dp blue box.

Hereβs another one:
Box(modifier.size(100.dp)) {
Image(
painter = painterResource(R.drawable.ic_launcher_background),
modifier = Modifier
.fillMaxSize()
.size(30.dp)
.offset(30.dp, 30.dp),
contentDescription = null
)
}
My expectation: βFill the parent (100dp), resize to 30dp, offset by (30,30). Should be a tiny 30Γ30dp image moved to position (30,30).β
So the output should be:

Reality: Nope. The output is:

The size(30.dp) modifier was completely ignored!
What the hell? The second size modifier did nothing?
Breakthrough!
I realized if I go back to square one and remember that βmodifiers are applied last-to-first, inside-to-outside,β from MΓ‘rton Braunβs article the outputs Iβm getting are correct because if I read the modifiers from last to first, the fillMaxSize() will override the size() modifier and the result will make sense. But does this overriding actually happen? The answer is No.
The Real Explanation
Modifiers donβt just βset sizesβ - they work with something called constraints. Think of constraints as rules about how big or small something can be.
When you use size(200.dp), youβre not just saying βmake this 200dp.β Youβre saying βthis MUST be exactly 200dp - no smaller, no bigger.β
So when you later add size(100.dp), itβs trying to say βthis MUST be exactly 100dp,β but it canβt, because the previous size modifier has already locked the constraints to exactly 200dp. The modifier chain continues with those constraintsβthey donβt change.
Itβs like if someone tells you βyou must be exactly 6 feet tallβ and then someone else says βyou must be exactly 5 feet tall.β The second rule canβt work because youβre already locked into the first one.
Testing This Theory: Multiple Size Modifiers
Let me show you a few examples I tested to confirm this:
Example 1: Double Size
Box(
modifier = Modifier
.size(150.dp)
.background(Color.Red)
.size(75.dp)
.background(Color.Green)
)
Result: 150Γ150dp box. Red background gets applied to the full 150dp area, then green background also gets applied to that same area (so you only see green). The size(75.dp) does nothing.

Example 2: Triple Size Chain
Box(
modifier = Modifier
.size(200.dp)
.size(100.dp)
.size(50.dp)
.background(Color.Blue)
)
Result: 200Γ200dp blue box. Only the first size modifier matters.

Checkpoint: So far weβve learned that we can think of modifier order from top to bottom, outside to inside, with repeated size modifiers having no effect.
Here are some cases that also need to be looked at:
Example 3: Size After Padding
Box(
modifier = Modifier
.size(120.dp)
.padding(10.dp)
.size(80.dp)
.background(Color.Yellow)
)
Here, a padding modifier lies between two size modifiers. Using padding(10.dp) is like creating a virtual area that has spacing of 10dp from the boundary of the box outside it. So now when you set size(80.dp), itβs being set for the inner area which is 10dp from the outside boundary. Also, 80dp fits within the available space after padding.

This isnβt limited to the padding modifier - think of it more as a concept, so it can be extrapolated. I tried the same with the wrapContentSize() modifier and the result was in line with this understanding.
What I Learned
So yeah, both those blog posts were right, just explaining different aspects:
- MΓ‘rtonβs βlast-to-first, inside-to-outsideβ helps you think about decoration and visual layering
- Marcinβs constraint-based explanation helps you understand why certain modifiers seem to get βignoredβ
My βread as you goβ approach works most of the time, but you need to understand that size-related modifiers create hard constraints that later modifiers have to respect.
The Real Takeaway
Instead of memorizing complex rules about modifier ordering, I just remember this:
- Read it naturally - most modifiers work exactly like youβd expect
- First size wins - when multiple size modifiers conflict, the first one locks it in
- Constraints flow down - each modifier receives constraints from the previous one and might modify them
This isnβt the most technically precise explanation (the constraint system is more complex), but itβs the mental model that actually works for me day-to-day. I stopped playing βmodifier rouletteβ and started being able to predict what my UI would look like before running it.
Hope this helps you get out of the trial-and-error loop too!