Pocket Studio Academy
HomePart 33.3

Modifiers — the decorator chain

Full course11 min read·3 questions

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 .

kotlin
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.

Think of it like this

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:

kotlin
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:

kotlin
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:

kotlin
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:

OrderDemo.ktkotlin
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}
9:41▲ ▮
background then padding
padding then background

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.

Tip

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

ModifierWhat 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:

DieFace.ktkotlin
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:

  1. Accept a modifier parameter. 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 a Box to work around you.
  2. Give it the default value Modifier. That means "no extra instructions", so callers who do not care can ignore it.
  3. 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:

GameScreen.ktkotlin
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.

Try it in Pocket Studio
  1. Open ComposeLab in Pocket Studio and open MainActivity.kt.
  2. Replace the body of DuelScreen with the OrderDemo code from this lesson.
  3. Add the imports Pocket Studio prompts for. You need androidx.compose.foundation.background, androidx.compose.ui.graphics.Color, androidx.compose.foundation.layout.Spacer and androidx.compose.foundation.layout.height.
  4. Tap Run. You should see one comfortable teal label and one cramped one.
  5. Now swap the two modifier lines on the first Text so they match the second. Run again — both labels should now look cramped. That is the whole lesson in one edit.
  6. Add .clip(RoundedCornerShape(8.dp)) before .background(...) on the first Text and Run. Rounded teal.
  7. Move that same .clip(...) line to after .background(...) and Run again. Square teal — because you clipped a shape that had already been painted underneath.
Error Doctor5 common errors
e: Unresolved reference: Modifier
MeansModifier lives in the core UI package and has to be imported like anything else.
FixAdd import androidx.compose.ui.Modifier. Note there is no s — it is ui.Modifier, not ui.Modifiers.
e: Unresolved reference: padding
MeansModifier functions are not all in one place. padding, size, fillMaxWidth and friends come from the layout package, and each one is imported separately.
FixAdd 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.
e: Unresolved reference: background
Meansbackground is not a layout modifier — it is a drawing one, and it lives in foundation rather than foundation.layout.
FixAdd import androidx.compose.foundation.background. The same goes for border and clickable; clip is different again, at androidx.compose.ui.draw.clip.
e: Type mismatch: inferred type is Int but Dp was expected
MeansYou wrote 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.
FixAdd .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.
The coloured background is in the wrong place — too big, or too small
MeansNot an error at all, just chain order. background before padding colours the padding in; background after padding leaves the padding transparent.
FixDecide which layer you want on the outside and put it first. Say the chain out loud from the top: "colour it, then leave a gap inside" versus "leave a gap, then colour what is left".
Recap
  • 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.
  • .dp is required on sizes — a bare number will not compile.
  • Accept modifier: Modifier = Modifier in your own composables and apply it to your outermost element, before your own modifiers.
  • Next: Column, Row and Box — the three containers that arrange everything else, and how to space and align what goes inside them.