Modifiers, and why order matters
Every composable you have written so far takes a modifier, and so does almost every composable
in the Compose library. A modifier is how you give something a size, padding, a background, a
border, a shape or a click handler. It looks like a list of settings. It is not: it is a chain,
applied in order, and swapping two links changes what the user sees and where they can tap. This
lesson runs the classic order mistakes on the emulator so you can see each one.
A modifier is a chain of wrappers
Text(
text = "background, then padding",
modifier = Modifier
.background(Color(0xFFFFE082))
.padding(16.dp),
)
Start with the empty Modifier, add a yellow background, then 16 dp of padding. The way to hold
this in your head is: each modifier applies to everything written after it. background comes
first, so it paints behind the padding and the text. padding comes second, so it only pushes
the text inwards. Think of each one as a box wrapped round the rest of the chain, the first one
outermost.
Each call returns a new Modifier; nothing is changed in place. Modifier on its own is the empty
chain you start from.
Padding before and after background
Put two texts in a Column on the emulator:
Text(
text = "background, then padding",
modifier = Modifier
.background(Color(0xFFFFE082))
.padding(16.dp),
)
Text(
text = "padding, then background",
modifier = Modifier
.padding(16.dp)
.background(Color(0xFFFFE082)),
)
The first shows a generous yellow block with the text sitting 16 dp inside it: the padding is inside the background, so it is painted yellow. The second shows a thin yellow strip hugging the letters, set 16 dp in from where the first one starts: the padding came first, so it is empty space outside the background.
Compose has no margin. Padding before a background or border is the margin; padding after it is the inner padding. One modifier, two meanings, decided entirely by position.
Padding before and after clickable
Order matters most where the user cannot see it. Two bordered texts, both meant to be tappable:
Text(
text = "clickable, then padding",
modifier = Modifier
.border(1.dp, Color.Gray)
.clickable { Log.d("Modifiers", "clicked: clickable, then padding") }
.padding(24.dp),
)
Text(
text = "padding, then clickable",
modifier = Modifier
.border(1.dp, Color.Gray)
.padding(24.dp)
.clickable { Log.d("Modifiers", "clicked: padding, then clickable") },
)
On screen they look identical: two grey boxes of the same size. On the emulator, the accessibility tree reported their tappable areas as:
| Version | Tappable area |
|---|---|
clickable, then padding |
The whole box, about 222 × 72 dp |
padding, then clickable |
Only the text, about 174 × 48 dp |
Tapping inside the border near the top-left corner logged clicked: clickable, then padding for
the first box and nothing at all for the second. The second box has a 24 dp dead zone all round
that looks tappable and is not. Users on a cheap phone, in a moving bus, with a thumb, miss small
targets constantly, and they blame the app. Material's guidance is a touch target of at least
48 × 48 dp; the accessibility lesson in module 4 comes back to it. The same rule applies to the
ripple: it fills only the clickable area, so with padding first, the ripple is a tight rectangle
round the text.
Put clickable before the padding that should be part of the tap area.
Clip before background
Text(
text = "clip, then background",
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(Color(0xFFA5D6A7))
.padding(16.dp),
)
Text(
text = "background, then clip",
modifier = Modifier
.background(Color(0xFFA5D6A7))
.clip(RoundedCornerShape(12.dp))
.padding(16.dp),
)
The first has rounded green corners. The second is a plain green rectangle: the background was
painted before the clip, so there was nothing for the clip to cut. Clip affects what is drawn
after it in the chain, so clip first, then paint. (background also takes a shape directly,
background(color, shape), which is shorter when you only need the colour rounded. clip is for
when the content, such as an image or a ripple, must be cut too.)
Size: fixed, fill and weight
By default a composable is as big as its content needs. Three ways to change that:
size(48.dp),width(…),height(…)set a fixed size. Use them for icons and avatars, not for text containers: a fixed height that fits "Onion" in English clips a longer Marathi label or a user's larger font setting.fillMaxWidth()takes all the width the parent offers.fillMaxWidth(0.5f)takes half.weight(1f)exists only inside aRoworColumn. It shares out the space left after the unweighted children are measured.
Row(modifier = Modifier.fillMaxWidth()) {
Text("Onion", modifier = Modifier.weight(1f).background(Color(0xFFE0E0E0)))
Text("₹2,450", modifier = Modifier.background(Color(0xFFB3E5FC)))
}
On the emulator the price takes exactly its own width at the right edge and "Onion" stretches to
fill everything else. That is the standard "label on the left, value on the right" row, and you
will write it constantly. Try weight outside a Row or Column and it does not compile: it is
defined on RowScope and ColumnScope, which are only available inside their lambdas.
The trap: the parameter you forgot to use
Every reusable composable takes modifier: Modifier = Modifier and should apply it to its
outermost element. Here is the mistake that breaks it:
@Composable
fun BrokenPriceCard(price: CommodityPrice, modifier: Modifier = Modifier) {
Card(modifier = Modifier.padding(8.dp)) {
Text(text = price.commodity, modifier = Modifier.padding(16.dp))
}
}
It compiles, and the build prints no warning about it. The only sign is that Android Studio shows
the unused modifier parameter greyed out in the editor, and you will not notice it. A caller writes BrokenPriceCard(price, Modifier.fillMaxWidth()) and the card stays narrow,
because the card used a fresh Modifier (capital M) and threw the caller's away. The fix is one
letter, and the convention for where it goes:
Card(modifier = modifier.padding(8.dp)) {
Start the root's chain from the modifier parameter, lower-case. Your own additions go after
it, so the caller's choices (size, outer padding) apply first. Inside the composable, children
start from Modifier, capital, because the caller's modifier belongs to the root only. PriceCard
does exactly this: Card(modifier = modifier) on the outside, Modifier.padding(16.dp) on the
inner Column.
The modifiers you will use most
| Modifier | What it does | Notes |
|---|---|---|
padding(16.dp), padding(horizontal = …, vertical = …) |
Space around what follows | Before a background or border it acts as margin |
background(color), background(color, shape) |
Paints behind what follows | |
border(1.dp, color), border(1.dp, color, shape) |
Draws a border round what follows | |
clip(shape) |
Cuts drawing of what follows to a shape | Put before background |
clickable { } |
Makes it tappable, adds a ripple and accessibility semantics | Put before inner padding |
size(…), width(…), height(…) |
Fixed size | Avoid on containers of text |
heightIn(min = 48.dp) |
Minimum size that can still grow | Safe for text and touch targets |
fillMaxWidth(), fillMaxHeight(), fillMaxSize() |
Take the space the parent offers | Optional fraction, e.g. 0.5f |
weight(1f) |
Share the leftover space | Inside Row/Column only |
wrapContentWidth(Alignment.End) |
Shrink to content and align within the space | |
aspectRatio(16f / 9f) |
Fix the ratio of width to height | Images, video |
offset(x = 4.dp) |
Move the drawing without changing layout | Neighbours do not move |
alpha(0.5f) |
Transparency | |
verticalScroll(rememberScrollState()) |
Lets a Column scroll |
Lesson 6 |
testTag("price-card") |
Lets UI tests find the element | Testing module |
Check your work
What "each modifier applies to everything written after it" means for padding then
background. The padding is outside the background, so it acts as a margin.
Why Compose has no margin modifier. Padding placed before a background or border already is
the margin.
What happened when you tapped the padded corner of the "padding, then clickable" box. Nothing; the tappable area is only the text inside the padding.
Why background then clip shows square corners. The background was drawn before the clip, so
the clip had nothing to cut.
Where weight can be used. Inside a Row or Column.
The bug in BrokenPriceCard. It ignores its modifier parameter, so the caller's
fillMaxWidth() has no effect.
Where the modifier parameter goes. At the start of the outermost element's chain.
Practice 3. PriceCard(price = …, modifier = Modifier.clickable { Log.d("Modifiers", price.market) }).
Because PriceCard applies its modifier to the Card, the whole card is the tap area.
Practice 4. "Lasalgaon" and "Onion" share the leftover width equally; the price keeps its own width. Weights divide the space left after unweighted children, in proportion to the numbers.
Practice 6. One correct answer:
@Composable
fun MarketChip(name: String, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(50)
Box(
modifier = modifier
.heightIn(min = 48.dp)
.clip(shape)
.border(1.dp, MaterialTheme.colorScheme.outline, shape)
.clickable { Log.d("Modifiers", "chip: $name") }
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center,
) {
Text(text = name)
}
}
heightIn first, so the minimum covers everything; clip before anything drawn, so the ripple is
rounded; border with the same shape; clickable before padding, so the padding is tappable.
Box stacks its content and centres it; module 4 covers it properly.
Practice
- Run the padding-and-background pair on the emulator and describe the difference in one sentence.
- Run the two clickable boxes, add a
Log.dto each, and tap the corners. Confirm which one logs. - Give
PriceCardaclickablethat logs the market, applied through the caller'smodifieronly: change the call site, notPriceCard. Check the whole card ripples. - Build the "Onion ... ₹2,450" row, then add a third
Text("Lasalgaon")withweight(1f)too. Predict how the space divides before running it. - Break it on purpose: put
Modifier.weight(1f)on aTextdirectly insidesetContent. Read the compiler's error and say which scope it is missing. - Harder: write
MarketChip(name: String, modifier: Modifier = Modifier)that draws the market name in a rounded, bordered, tappable chip with a 48 dp minimum height, where the whole chip, including its padding, is tappable and the ripple is rounded. Get the order ofclip,border,clickable,heightInandpaddingright, and explain each position.
Official documentation
- Compose modifiers — The official guide, including why order matters and scope-specific modifiers.
- List of Compose modifiers — Every built-in modifier, grouped by purpose, for when the table above runs out.
- Compose layout basics — How measurement works, which explains
fillMaxWidthandweight; module 4 covers it.
Next: previews, and seeing a change without running the app.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.
About the internship