Modifiers — the decorator chain
Size, padding, background, corners, borders and taps all come from one place in Compose: the modifier chain. Learn how the chain is read, why the order of it changes what you see, and the one convention every reusable composable follows.
Everything that is not the content
Your Greeting from the last lesson works, and it looks terrible. The text is jammed into the top-left corner with no breathing room, no background, no size of its own.
Text is not going to fix that, and it should not try. A Text composable has one job: put these characters on screen. Where they sit, how much space they take, what colour is behind them, whether tapping them does anything — none of that is text's business.
All of it comes from one place instead: the .
1Text(
2 text = "Hello from Compose",
3 modifier = Modifier
4 .background(Color(0xFFB7F1E6))
5 .padding(16.dp)
6)Almost every composable in the entire Compose library takes a modifier parameter, and they all mean the same thing by it. Learn the chain once and you have learned the layout half of Compose.
Picture a parcel moving down a packing line.
The thing itself — a mug — goes in at one end. Then it passes station after station. One station wraps it in bubble wrap. The next puts it in a box. The next adds a label. Each station wraps another layer around whatever arrived.
A modifier chain is that packing line, read top to bottom. The composable is the mug. Each .something() is a station.
And here is the part that catches everyone: swap two stations and you get a different parcel. Bubble wrap, then box, gives you a padded mug in a box. Box, then bubble wrap, gives you a boxed mug wrapped in bubble wrap on the outside. Both are "a mug, a box and bubble wrap". They are not the same parcel.
What a Modifier actually is
Modifier on its own is an object meaning "no instructions at all". Every function you call on it hands back a new modifier carrying one more instruction:
1val a = Modifier
2val b = a.padding(16.dp)
3val c = b.background(Color.White)a is still empty. b still only has padding. Modifiers are — nothing is ever changed in place, which is why you can safely store one in a and reuse it in three composables without them interfering with each other.
In practice you never write it as three lines. You chain:
1Modifier
2 .padding(16.dp)
3 .background(Color.White)One call per line, indented. That is the house style for this course, and it is not just tidiness — with the 60-character limit of Pocket Studio's editor, a chain of four modifiers on one line is unreadable on a phone.
Reading the chain: top is outside
This is the single rule that makes order make sense:
The first modifier in the chain is the outermost layer. The composable itself is innermost.
So Modifier.padding(16.dp).background(Color.White) means: padding on the outside, then a white background, then the content. The white starts after the padding — the padding is transparent.
Turn it around and you get the opposite:
1Modifier
2 .background(Color.White)
3 .padding(16.dp)Now white is the outer layer, so the white area includes the 16 units of padding, and the content sits in the middle of a white block.
Here are both, side by side, for real:
1@Composable
2fun OrderDemo() {
3 Column(modifier = Modifier.padding(20.dp)) {
4 Text(
5 text = "background then padding",
6 modifier = Modifier
7 .background(Color(0xFFB7F1E6))
8 .padding(12.dp)
9 )
10 Spacer(Modifier.height(16.dp))
11 Text(
12 text = "padding then background",
13 modifier = Modifier
14 .padding(12.dp)
15 .background(Color(0xFFB7F1E6))
16 )
17 }
18}Same two modifiers, swapped. The teal is where the background lands.
The first block is a comfortable teal label. The second is a cramped teal label floating in white space. Neither is wrong — but only one of them is what you meant, and now you can tell which is which by reading the chain.
The same rule bites with taps. Modifier.clickable { }.padding(16.dp) makes the padded area tappable, because clickable is outside the padding. Modifier.padding(16.dp).clickable { } makes only the content tappable, and the padding is dead space. For a small icon, that is the difference between a comfortable button and one people keep missing.
The modifiers you will use constantly
| Modifier | What it does |
|---|---|
.padding(16.dp) | Space on all four sides |
.padding(horizontal = 20.dp, vertical = 8.dp) | Space on two axes |
.fillMaxWidth() | Be as wide as the parent allows |
.fillMaxSize() | Be as wide and tall as allowed |
.size(64.dp) | Exactly this square |
.height(9.dp) / .width(120.dp) | One dimension |
.background(Color(0xFFFFD9E2)) | Paint behind the content |
.clip(RoundedCornerShape(24.dp)) | Cut the corners off |
.border(3.dp, Color.Red, shape) | Draw an outline |
.clickable { } | Make it respond to taps |
.weight(1f) | Share the leftover space (inside a Row or Column only) |
Two of those need a word of explanation.
.dp is a . Phones vary enormously in how many real pixels they cram into a centimetre, so Compose never asks you for pixels. 16.dp is about a fingernail's width on every phone ever made. You get it with import androidx.compose.ui.unit.dp, and you write it as a suffix on a number, because dp is an extension property on Int — exactly the Kotlin feature from Lesson 1.18.
fillMaxWidth takes an optional fraction: fillMaxWidth(0.5f) means half the available width. Dice Duel uses that trick to draw a score bar filling up.
The convention every reusable composable follows
Look at a real composable from Dice Duel:
1@Composable
2fun DieFace(
3 value: Int,
4 modifier: Modifier = Modifier,
5 size: Dp = 160.dp,
6 onRoll: () -> Unit = {}
7) {
8 Canvas(
9 modifier = modifier
10 .size(size)
11 .clickable { onRoll() }
12 ) {
13 drawDie(value)
14 }
15}Three details there are a convention followed by the entire Compose ecosystem, and you should follow it too:
- Accept a
modifierparameter. Anyone using your composable will eventually want to add padding or a weight from the outside. If you do not accept one, they cannot, and they have to wrap your composable in aBoxto work around you. - Give it the default value
Modifier. That means "no extra instructions", so callers who do not care can ignore it. - Make it the first parameter that has a default, and apply it to your outermost element, before any of your own modifiers.
modifier.size(size)— the caller's instructions go on the outside, yours on the inside. That way a caller's.padding()sits outside your.size(), which is what they expect.
Here is that paying off. In GameScreen.kt, the caller adds a spin without DieFace knowing anything about animation:
1DieFace(
2 value = die,
3 size = 172.dp,
4 modifier = Modifier.graphicsLayer {
5 rotationZ = spin
6 },
7 onRoll = { roll() }
8)Read that chain out loud and it is almost English: round the corners, fill it pink, outline it red, then leave a gap before the content.
- Open ComposeLab in Pocket Studio and open
MainActivity.kt. - Replace the body of
DuelScreenwith theOrderDemocode from this lesson. - Add the imports Pocket Studio prompts for. You need
androidx.compose.foundation.background,androidx.compose.ui.graphics.Color,androidx.compose.foundation.layout.Spacerandandroidx.compose.foundation.layout.height. - Tap Run. You should see one comfortable teal label and one cramped one.
- Now swap the two modifier lines on the first
Textso they match the second. Run again — both labels should now look cramped. That is the whole lesson in one edit. - Add
.clip(RoundedCornerShape(8.dp))before.background(...)on the first Text and Run. Rounded teal. - Move that same
.clip(...)line to after.background(...)and Run again. Square teal — because you clipped a shape that had already been painted underneath.
Modifier lives in the core UI package and has to be imported like anything else.import androidx.compose.ui.Modifier. Note there is no s — it is ui.Modifier, not ui.Modifiers.padding, size, fillMaxWidth and friends come from the layout package, and each one is imported separately.import androidx.compose.foundation.layout.padding. Import the individual function, not a package — androidx.compose.foundation.layout.* also works but Pocket Studio's suggestion is usually better.background is not a layout modifier — it is a drawing one, and it lives in foundation rather than foundation.layout.import androidx.compose.foundation.background. The same goes for border and clickable; clip is different again, at androidx.compose.ui.draw.clip.padding(16) instead of padding(16.dp). A bare number is not a size — Compose refuses to guess whether you meant pixels, points or something else..dp to the number and import androidx.compose.ui.unit.dp. The only modifier that takes a plain number is weight, which wants a Float such as 1f.background before padding colours the padding in; background after padding leaves the padding transparent.- A carries everything about a composable that is not its content: size, space, colour behind it, corners, borders, taps.
- Each
.something()returns a new modifier. They are and safe to reuse. - The first modifier in the chain is the outermost layer. Order changes the result, always.
.dpis required on sizes — a bare number will not compile.- Accept
modifier: Modifier = Modifierin your own composables and apply it to your outermost element, before your own modifiers. - Next:
Column,RowandBox— the three containers that arrange everything else, and how to space and align what goes inside them.