Exploring Modifier.Node for creating custom Modifiers in Jetpack Compose
In this article, you will learn how to create custom modifiers using the three primary APIs, Modifier.then(), Modifier.composed(), and Modifier.Node

In modern Android development, Jetpack Compose has become a modern UI toolkit, not only for building Android UIs but also for supporting Kotlin Multiplatform projects. Its ecosystem continues to grow rapidly, making it an essential part of today’s Android development.
At the core of Compose lies the Modifier
. A modifier is more than just a way to apply styles and themes, it’s a design pattern that enables components to remain independent while still being easily customizable. By creating custom modifiers, you can encapsulate behavior once and reuse it throughout your app, making your UI code both cleaner and more flexible.
In this article, we’ll explore how to create custom modifiers with primary three different APIs, Modifier.then()
, Modifier.composed()
, and Modifier.Node
, and demonstrates how RevenueCat’s Android SDK leverages Modifier.Node
to deliver better UI performance.
Modifier.then
This is the easiest way to create a custom modifier by using the Modifier.then()
function, letting you combine multiple modifiers through chaining and return the final result, as shown in the example below:
1@Composable
2fun Modifier.composableModifier(): Modifier {
3 val color = LocalContentColor.current.copy(alpha = 0.5f)
4 return this then Modifier.background(color)
5}
6
7@Composable
8fun MyComposable() {
9 val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
10}
The infix fun Modifier.then(other: Modifier)
is the fundamental operator responsible for chaining modifiers in Jetpack Compose. It does not create a flat list but rather a recursive, two-element data structure called CombinedModifier
. This structure functions as a form of cons list or linked list, optimized for the specific directional traversal patterns required by the Compose UI toolkit.
Let’s say there’s a simple modifier, that allows developers to chain modifiers declaratively to build up the properties of a UI element:
1Modifier
2 .padding(16.dp) // First (outer)
3 .background(Color.Blue) // Second
4 .clickable { } // Third (inner)
One might assume this creates a List<Modifier.Element>
. However, this is not the case. The Modifier.then()
function, which is implicitly called by each subsequent modifier in the chain, constructs a nested, tree-like structure. This design is a deliberate and crucial architectural choice for performance and correctness.
If you look into the internal codes of the Modifier.then()
function, you’ll find it internally creates a new CombinedModifier
instance.
1
2infix fun then(other: Modifier): Modifier =
3 if (other === Modifier) this else CombinedModifier(this, other)
4
5 class CombinedModifier(internal val outer: Modifier, internal val inner: Modifier) : Modifier {
6 override fun <R> foldIn(initial: R, operation: (R, Modifier.Element) -> R): R =
7 inner.foldIn(outer.foldIn(initial, operation), operation)
8
9 override fun <R> foldOut(initial: R, operation: (Modifier.Element, R) -> R): R =
10 outer.foldOut(inner.foldOut(initial, operation), operation)
11
12 override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
13 outer.any(predicate) || inner.any(predicate)
14
15 override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
16 outer.all(predicate) && inner.all(predicate)
17
18 override fun equals(other: Any?): Boolean =
19 other is CombinedModifier && outer == other.outer && inner == other.inner
20
21 override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode()
22
23 override fun toString() =
24 "[" +
25 foldIn("") { acc, element ->
26 if (acc.isEmpty()) element.toString() else "$acc, $element"
27 } +
28 "]"
29}
Its sole purpose is to take two modifiers, the existing chain (this
) and a new one (other
), and join them using the CombinedModifier
class. The CombinedModifier
is not a collection; it is a simple, two-element container. It acts as a node in a linked list.
1class CombinedModifier(internal val outer: Modifier, internal val inner: Modifier) : Modifier
outer
: This holds the modifier that came *before* in the chain.
inner
: This holds the modifier that was just added.
When you write Modifier.padding(8.dp).background(Color.Red)
, the resulting structure is: CombinedModifier(outer = CombinedModifier(outer = padding, inner = background), inner = clickable)
This creates a left-associative, recursive structure that looks like this: ((padding then background) then clickable)
This “linked list” structure is the key to understanding how modifiers are processed. The creative CombinedModifier
structure is realized through its traversal methods. A standard list has one way to iterate (from start to end). CombinedModifier
has two, with opposite directions, which are important for the different phases of the UI pipeline.
So, ultimately, Modifier.then
is the simple yet optimized function that enables the entire declarative and chainable modifier system, forming the backbone of UI customization in Jetpack Compose.
Modifier.composed
Another available API is Modifier.composed()
, which lets you create a custom modifier that can leverage composable functions inside the lambda parameter.
1fun Modifier.myStatefulModifier() = composed {
2 val myState by remember { mutableStateOf(0f) }
3 // ... use myState
4 Modifier.drawBehind { /* ... */ }
5}
Internally, it behaves much like Modifier.then
, since it creates a new instance of ComposedModifier
and combines it with the existing modifier using Modifier.then
, as shown below:
1fun Modifier.composed(
2 inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
3 factory: @Composable Modifier.() -> Modifier,
4): Modifier = this.then(ComposedModifier(inspectorInfo, factory))
While Modifier.composed
makes it easy to create custom modifiers, this API is no longer recommended because of the performance issues it introduces. The method allows you to invoke @Composable
functions, such as remember
, when creating a modifier instance. This is useful for attaching an instance-specific state, which lets the same modifier object be reused safely in multiple places.
However, if you use Modifier.composed
without actually calling any @Composable
functions, it becomes unnecessary overhead. In this case, the modifier is no longer considered “skippable”, which forces the runtime to perform additional work during recomposition. As a result, every recomposition re-executes the modifier chain to update state, leading to degraded performance.
Modifier.Node: The state and logic
Modifier.Node
has been introduced since Compose UI version 1.3.0, and now this is the most recommended API for creating a custom Modifier against Modifier.composed
. While the Modifier.composed
factory provided an early mechanism for creating custom, stateful modifiers, it came with a significant performance cost: the creation of a new subcomposition for every modifier instance. This overhead could lead to degraded performance, especially in dynamic UIs like lists.
The Modifier.Node
system was introduced to solve this problem directly. It is a new, lower-level API that allows developers to create stateful modifiers that are lightweight, lifecycle-aware, and integrated into Compose’s core rendering pipeline, without the overhead of subcomposition.
Modifier.Node
The Modifier.Node
implementation is the heart of your custom modifier. It is a stateful, mutable class that holds the modifier’s properties and implements its core logic. Critically, a Modifier.Node
instance can survive across multiple recompositions and can even be reused if its configuration remains the same.
1// The Node: Holds state (color) and logic (drawCircle)
2private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
3 override fun ContentDrawScope.draw() {
4 // Implements the drawing logic
5 drawCircle(color)
6 }
7}
The Element
has two primary responsibilities:
1. create(): Instantiates the Modifier.Node
the very first time the modifier is applied.
2. update(node: CircleNode): This is the key to performance. On subsequent recompositions, if the Element
has changed (determined by its equals
method), the update
function is called on the existing Node
instance. This allows you to update the Node's
state without the expensive cost of creating a new object.
Note: It is critical that your
ModifierNodeElement
correctly implementsequals
andhashCode
. Using a data class is the easiest way to ensure this. Without a correct implementation,update
may be called unnecessarily, defeating the performance benefits.
The Modifier Factory
This is the clean, public API surface function that developers will use. It simply uses the then
operator to chain the new ModifierNodeElement
onto the existing modifier chain.
1// The Factory: The clean public API
2fun Modifier.circle(color: Color): Modifier = this.then(CircleElement(color))
You can now use your custom modifier just like any other built-in modifier function. By separating the stateless factory (Element
) from the stateful worker (Node
), it minimizes allocations and maximizes object reuse. Its rich set of specialized node types and lifecycle-aware APIs provides developers with direct, safe, and efficient access to every phase of the UI pipeline.
RevenueCat’s Android SDK also leverages Modifier.Node to achieve better UI performance. We recently migrated from the older Modifier.composed() API to Modifier.Node
when implementing the placeholder feature. You can check out Placeholder.kt file to see the full changes and how this works in a real-world example.
Conclusion
In this article, you’ve explored how to create custom modifiers using the three primary APIs, Modifier.then()
, Modifier.composed()
, and Modifier.Node
, and learned how the RevenueCat SDK leverages Modifier.Node
to improve UI performance. Understanding the trade-offs between these approaches is key: while Modifier.then()
is simple and lightweight, Modifier.composed()
introduces performance costs when misused, and Modifier.Node
offers a more modern, efficient way to build custom behaviors. By applying these patterns thoughtfully, you can create modifiers that are not only reusable and expressive but also optimized for performance, making your Compose UIs more stable.
You might also like
- Blog post
Mark your models as stable with the Compose runtime annotation library
In this article, we’ll look at how to address this issue using the new compose-runtime-annotation library.
- Blog post
remember vs rememberSaveable: deep dive into state management and recomposition in Jetpack Compose
Understanding the differences between remember and rememberSaveable by exploring their internal mechanisms, and how they relate to state and recomposition.
- Blog post
Turn Your App into Revenue: Building Paywalls in Android With Jetpack Compose
In this article, you'll learn how to seamlessly implement in-app subscriptions and paywall features in Android using Jetpack Compose and the RevenueCat SDK.