Create custom modifiers

Compose provides many modifiers for common behaviors right out of the box, but you can also create your own custom modifiers.

Modifiers have multiple parts:

  • A modifier factory
    • This is an extension function on Modifier, which provides an idiomatic API for your modifier and allows modifiers to be chained together. The modifier factory produces the modifier elements used by Compose to modify your UI.
  • A modifier element
    • This is where you can implement the behavior of your modifier.

There are multiple ways to implement a custom modifier depending on the functionality needed. Often, the simplest way to implement a custom modifier is to implement a custom modifier factory that combines other already defined modifier factories. If you need more custom behavior, implement the modifier element using the Modifier.Node APIs, which are lower level but provide more flexibility.

Chain existing modifiers together

It is often possible to create custom modifiers by using existing modifiers. For example, Modifier.clip() is implemented using the graphicsLayer modifier. This strategy uses existing modifier elements, and you provide your own custom modifier factory.

Before implementing your own custom modifier, see if you can use the same strategy.

fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)

Or, if you find you are repeating the same group of modifiers often, you can wrap them into your own modifier:

fun Modifier.myBackground(color: Color) = padding(16.dp)
    .clip(RoundedCornerShape(8.dp))
    .background(color)

Create a custom modifier using a composable modifier factory

You can also create a custom modifier using a composable function to pass values to an existing modifier. This is known as a composable modifier factory.

Using a composable modifier factory to create a modifier also lets you use higher level compose APIs, such as animate*AsState and other Compose state backed animation APIs. For example, the following snippet shows a modifier that animates an alpha change when enabled/disabled:

@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return this then Modifier.graphicsLayer { this.alpha = alpha }
}

If your custom modifier is a convenience method to provide default values from a CompositionLocal, the easiest way to implement this is to use a composable modifier factory:

@Composable
fun Modifier.fadedBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

This approach has some caveats, which are detailed in the following sections.

CompositionLocal values are resolved at the call site of the modifier factory

When creating a custom modifier using a composable modifier factory, composition locals take the value from the composition tree where they are created, not used. This can lead to unexpected results. For example, consider the composition local modifier example mentioned previously, implemented slightly differently using a composable function:

@Composable
fun Modifier.myBackground(): Modifier {
    val color = LocalContentColor.current
    return this then Modifier.background(color.copy(alpha = 0.5f))
}

@Composable
fun MyScreen() {
    CompositionLocalProvider(LocalContentColor provides Color.Green) {
        // Background modifier created with green background
        val backgroundModifier = Modifier.myBackground()

        // LocalContentColor updated to red
        CompositionLocalProvider(LocalContentColor provides Color.Red) {

            // Box will have green background, not red as expected.
            Box(modifier = backgroundModifier)
        }
    }
}

If this is not how you expect your modifier to work, use a custom Modifier.Node instead, as composition locals will be correctly resolved at the usage site and can be safely hoisted.

Composable function modifiers are never skipped

Composable factory modifiers are never skipped because composable functions that have return values cannot be skipped. This means your modifier function will be called on every recomposition, which may be expensive if it recomposes frequently.

Composable function modifiers must be called within a composable function

Like all composable functions, a composable factory modifier must be called from within composition. This limits where a modifier can be hoisted to, as it can never be hoisted out of composition. In comparison, non-composable modifier factories can be hoisted out of composable functions to allow easier reuse and improve performance:

val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations

@Composable
fun Modifier.composableModifier(): Modifier {
    val color = LocalContentColor.current.copy(alpha = 0.5f)
    return this then Modifier.background(color)
}

@Composable
fun MyComposable() {
    val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}

Implement custom modifier behavior using Modifier.Node

Modifier.Node is a lower level API for creating modifiers in Compose. It is the same API that Compose implements its own modifiers in and is the most performant way to create custom modifiers.

Implement a custom modifier using Modifier.Node

There are three parts to implementing a custom modifier using Modifier.Node:

  • A Modifier.Node implementation that holds the logic and state of your modifier.
  • A ModifierNodeElement that creates and updates modifier node instances.
  • An optional modifier factory, as detailed previously.

ModifierNodeElement classes are stateless and new instances are allocated each recomposition, whereas Modifier.Node classes can be stateful and will survive across multiple recompositions, and can even be reused.

The following section describes each part and shows an example of building a custom modifier to draw a circle.

Modifier.Node

The Modifier.Node implementation (in this example, CircleNode) implements the functionality of your custom modifier.

// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
    override fun ContentDrawScope.draw() {
        drawCircle(color)
    }
}

In this example, it draws the circle with the color passed in to the modifier function.

A node implements Modifier.Node as well as zero or more node types. There are different node types based on the functionality your modifier requires. The preceding example needs to be able to draw, so it implements DrawModifierNode, which lets it override the draw method.

The available types are as follows:

Node

Usage

Sample Link

LayoutModifierNode

A Modifier.Node that changes how its wrapped content is measured and laid out.

Sample

DrawModifierNode

A Modifier.Node that draws into the space of the layout.

Sample

CompositionLocalConsumerModifierNode

Implementing this interface lets your Modifier.Node read composition locals.

Sample

SemanticsModifierNode

A Modifier.Node that adds semantics key/value for use in testing, accessibility, and similar use cases.

Sample

PointerInputModifierNode

A Modifier.Node that receives PointerInputChanges.

Sample

ParentDataModifierNode

A Modifier.Node that provides data to the parent layout.

Sample

LayoutAwareModifierNode

A Modifier.Node which receives onMeasured and onPlaced callbacks.

Sample

GlobalPositionAwareModifierNode

A Modifier.Node which receives an onGloballyPositioned callback with the final LayoutCoordinates of the layout when the global position of the content may have changed.

Sample

ObserverModifierNode

Modifier.Nodes that implement ObserverNode can provide their own implementation of onObservedReadsChanged that will be called in response to changes to snapshot objects read within an observeReads block.