A Modifier is a linked list, and that explains almost everything about it
Modifier looks like a fluent builder but is an ordered immutable linked list. Knowing its actual shape explains chaining, reuse, the modifier parameter convention, and why some combinations do nothing.

Day 19 of 100. Yesterday's containers decide where children go. Modifiers decide everything else about a single element — size, padding, background, click handling — and they are a much simpler object than they look.
The symptom
A reusable card that ignores the caller:
@Composable
fun ProfileCard(user: User) {
Card(modifier = Modifier.padding(16.dp)) { … }
}
// Caller wants it to fill the width. Nothing happens.
ProfileCard(user) // no way to pass anything in
The caller can't size it, can't add a click, can't pad it differently. Every attempt
ends in editing ProfileCard and adding another boolean.
Why the obvious fix fails
The obvious fix is parameters for the things callers ask for:
@Composable
fun ProfileCard(user: User, fillWidth: Boolean = false, extraPadding: Dp = 0.dp) { … }
Six months later there are nine of them, they interact, and the tenth request needs a tenth. The component is configurable in exactly the ways somebody once needed, and rigid in every other.
Compose's answer is one parameter that covers all of them at once — but it only works if you understand what the parameter is.

The actual mechanism
Modifier is an immutable, ordered linked list. Each call in a chain returns a new
Modifier whose head is the new element and whose tail is everything before it:
Modifier
.padding(16.dp) // element 1
.background(Blue) // element 2
.size(48.dp) // element 3
Three elements, in that order, forever. Nothing mutates: Modifier.padding(16.dp) does
not modify Modifier; it produces a new chain. That immutability is why a modifier can
be hoisted into a val, shared between composables, and stored in a constant without
any risk of one caller's chain leaking into another's.
private val CardShape = Modifier.clip(RoundedCornerShape(12.dp)).background(Surface)
// Safe to use in twenty places — each `.then` produces a fresh chain
Box(CardShape.size(64.dp))
Box(CardShape.fillMaxWidth())
Chaining is then under the hood, and Modifier itself is the empty element — the
identity for the operation. That's why Modifier alone is a valid argument, and why a
conditional modifier is written by picking an element or the empty one:
Modifier
.fillMaxWidth()
.then(if (selected) Modifier.border(2.dp, Blue) else Modifier)
Not a special API — just the identity element doing what identity elements do.
The convention that makes components composable
Every composable that emits UI should take a modifier parameter, and the convention
is precise enough to be worth stating exactly:
@Composable
fun ProfileCard(
user: User, // required params first
modifier: Modifier = Modifier, // FIRST optional param, named `modifier`
) {
Card(modifier = modifier) { … } // applied to the ROOT, first in the chain
}
Four rules, all load-bearing:
Named modifier. Tooling, lint and every reader expect it.
Defaults to Modifier. The empty chain, so callers who don't care pass nothing.
First optional parameter. So it can be passed positionally after the required ones without naming every other default.
Applied to the root, and applied first. This is the one that gets broken. If you
write Card(modifier = Modifier.padding(8.dp).then(modifier)), you've silently forced
padding the caller can't remove. The caller's modifier goes first; your own additions
chain after it.
Get those right and ProfileCard needs no fillWidth parameter, no extraPadding, and
no tenth boolean:
ProfileCard(user, Modifier.fillMaxWidth().clickable { open(user) })
Only one modifier parameter
A component takes exactly one modifier, for its root. When an inner element needs
configuring, that's a signal the component should be split, or should take a slot:
// Wrong: two modifier parameters
@Composable
fun Row(modifier: Modifier, textModifier: Modifier)
// Right: the caller composes the inner element themselves
@Composable
fun Row(modifier: Modifier = Modifier, label: @Composable () -> Unit)
That's Day 13's slot argument arriving from the other direction — containers own layout, callers own content.
Modifier.Node, and why the old API is going away
Modifier elements used to be implemented as composed { } — a factory that ran a
composable lambda to build state per usage. It worked, and it allocated on every
recomposition and prevented the compiler from skipping.
The current API is Modifier.Node: a long-lived object attached to the layout node,
created once and updated in place.
// Old: allocates per recomposition
fun Modifier.highlight(color: Color) = composed {
val alpha by animateFloatAsState(if (color == Red) 1f else 0.5f)
drawBehind { drawRect(color.copy(alpha = alpha)) }
}
// Current: one node, updated
fun Modifier.highlight(color: Color) = this then HighlightElement(color)
You mostly meet this as a consumer rather than an author — the built-in modifiers were
migrated years ago, and the practical effect is that chains are cheaper than they used
to be. It matters when reading older answers online: composed { } in a snippet is a
reliable sign the snippet predates the current guidance. Writing your own nodes is
Day 22.
What a modifier element actually is
Each element in the chain is one of a small number of kinds, and knowing which explains what a modifier can and cannot do:
- Layout modifiers (
padding,size,offset) participate in measurement — they wrap the child's measure call and change its constraints or reported size. - Draw modifiers (
background,border,drawBehind) add drawing before or after the content. - Pointer input modifiers (
clickable,draggable) attach gesture handling. - Semantics modifiers (
contentDescription,testTag) add to the accessibility and test trees.
They live in one chain because a UI element's size, appearance and behaviour are all just decorations on the same node — and because a single ordered list is dramatically cheaper than four separate ones.
Scoped modifiers
Some modifiers only exist inside a particular container, enforced by the type system rather than by documentation:
Row {
Text("a", Modifier.weight(1f)) // RowScope.weight — compiles
Text("b", Modifier.align(Alignment.CenterVertically))
}
Box {
Text("c", Modifier.weight(1f)) // does NOT compile: no weight in BoxScope
}
weight is an extension on RowScope/ColumnScope; align takes a different
Alignment type in each scope. This is why copying a modifier chain between a Row and
a Box sometimes fails to compile — the chain referenced a member that only that scope
provides. The error is doing you a favour: weight in a Box has no meaning, since a
Box has no main axis to divide.
How to prove it
The chain is inspectable at runtime, which makes the linked-list claim concrete rather than theoretical:
val m = Modifier.padding(8.dp).background(Color.Red).size(40.dp)
m.foldIn(0) { count, element -> Log.d("Mod", element.toString()); count + 1 }
.also { Log.d("Mod", "elements: $it") }
foldIn walks the chain outside-in; foldOut walks it the other way. The log prints
three elements in declaration order — that ordering is the subject of tomorrow's post,
because it changes what you see on screen.
What this generalizes to
The pattern is decoration as data. Rather than a styling language or a property bag, a modifier is an ordered list of transformations, and the component's own behaviour is just what remains after they're applied. Middleware chains in web servers have the same shape, and gain the same property: composition is associative, so callers can build and share fragments without knowing what they'll be attached to.
Tomorrow, Day 20: why Modifier.padding(8.dp).background(Red) and
Modifier.background(Red).padding(8.dp) produce visibly different results.
Day 19 of a 100-day series on Jetpack Compose, working through the official documentation in order. Source: Compose modifiers.