graphicsLayer, Draw Modifiers, and Custom Drawing in Compose

Quick answer: use
graphicsLayerfor transforms and layer effects on a composable; usedrawBehindfor decoration behind existing content; usedrawWithContentwhen drawing order matters; and usedrawWithCacheonly when a drawing-only object such as aBrush,Path, or measured text result should be reused while size and read state stay unchanged.
These APIs all affect rendering, but they solve different problems. Picking by intent keeps modifier chains clear and avoids treating layers or cache allocation as automatic performance improvements.
Choose the smallest correct tool
| Need | Prefer |
|---|---|
| Animate alpha, scale, rotation, translation, shadow, clip, or a render effect | graphicsLayer |
| Paint a border, gradient, or shape behind a composable | drawBehind |
| Draw before, after, or around the composable’s own content | drawWithContent |
| Reuse a size-dependent drawing object | drawWithCache |
| The entire component is custom geometry | Canvas |
Android’s graphics modifiers guide identifies drawWithContent, drawBehind, and drawWithCache as the three main draw modifiers. The key distinction is drawing order and object lifetime—not visual style.
Use graphicsLayer for transform and layer properties
graphicsLayer applies properties to a composable’s rendered layer: alpha, scale, translation, rotation, shape clipping, shadow elevation, color filtering, and effects.
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
val scale by animateFloatAsState(
targetValue = if (selected) 1.04f else 1f,
label = "card scale",
)
val alpha by animateFloatAsState(
targetValue = if (enabled) 1f else 0.45f,
label = "card alpha",
)
val cardModifier = Modifier.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
}The lambda overload is especially useful for animated or state-backed properties. The graphicsLayer reference says reads inside the block update layer properties without forcing recomposition and relayout. Keep work inside that block small because Compose can invoke it multiple times.
This is a rendering transform, not a layout transform. Scaling a card does not make siblings reserve more space; use layout APIs when the actual measured size or placement must change. For state-driven values, see animate*AsState.
Use drawBehind for decoration
drawBehind draws before the composable content. It is a natural fit for a custom background or border that depends on the final measured size.
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
val outlinedModifier = Modifier.drawBehind {
val radius = 16.dp.toPx()
drawRoundRect(
color = Color(0xFF14B8A6),
cornerRadius = CornerRadius(radius),
style = Stroke(width = 2.dp.toPx()),
)
}Apply it to an existing Text, Card, or layout. The modifier receives a DrawScope, so size and density conversion are available at draw time. Use a normal background or border when they already produce the desired result; custom drawing should earn its complexity.
Use drawWithContent when ordering is the feature
drawWithContent gives you control over when the child content is drawn. Call drawContent() exactly where the content should appear relative to custom draw commands.
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
val spotlightModifier = Modifier.drawWithContent {
drawContent()
drawCircle(
color = Color.Black.copy(alpha = 0.18f),
radius = size.minDimension / 3f,
center = center,
)
}Calling the custom operation before drawContent() paints behind it; calling it afterward paints on top. Do not forget drawContent() unless intentionally replacing the child rendering. This is the base behavior behind drawBehind.
Use drawWithCache only for real reusable draw objects
drawWithCache creates drawing resources once and keeps them until the draw size or state read in the cache block changes. Use it for a Brush, Path, Shader, or expensive text measurement that is used only for drawing.
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
val gradientModifier = Modifier.drawWithCache {
val brush = Brush.linearGradient(
colors = listOf(Color(0xFF8B5CF6), Color(0xFF14B8A6)),
)
onDrawBehind {
drawRoundRect(brush = brush)
}
}The cache invalidates when its drawing size changes or its read state changes. It is not a default wrapper for every modifier: the official documentation notes that using it without a cache-worthy object adds unnecessary lambda allocations.
If an object belongs to composition and is not tied to drawing bounds, remember can be a clearer owner. If it only exists to draw and depends on size, drawWithCache is the stronger fit.
Understand compositing before forcing an offscreen layer
graphicsLayer does not automatically mean a separate bitmap is always created. The default CompositingStrategy.Auto chooses based on layer properties. An alpha below 1f or a render effect can require offscreen rendering; CompositingStrategy.Offscreen always renders the layer into an offscreen buffer first.
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer
val maskedModifier = Modifier.graphicsLayer(
compositingStrategy = CompositingStrategy.Offscreen,
)Force Offscreen only for a concrete need such as a blend-mode mask or deliberate layer isolation. Its buffer is sized to the drawing area and clips drawing to that region. The compositing strategy guide documents both the clipping behavior and the default strategy.
Keep custom drawing in the drawing phase
State read by graphicsLayer, Canvas, or a draw modifier can update drawing without recreating normal layout. That is useful for a visual-only transform, but it does not make heavy per-frame work free. Build paths, gradients, and text layouts deliberately, then profile a real animation or scroll interaction.
The Compose UI phases guide explains why this distinction matters. For full-surface geometry, use Canvas drawing in Compose; for normal visual effects on existing content, prefer the modifier that directly expresses the relationship.
Common mistakes
Using graphicsLayer to change layout
Layer scale and translation alter the rendered result, not how siblings are measured. Animate layout values when the surrounding layout must respond.
Caching a trivial draw call
drawWithCache is for created reusable resources. A direct drawRect or drawCircle usually has nothing meaningful to cache.
Forgetting drawContent
Inside drawWithContent, omitting drawContent() hides the child. This is occasionally intentional, but often an ordering bug.
Forcing Offscreen to “improve performance”
Offscreen rendering has a purpose and a cost. Use it for correct compositing or a measured rendering benefit, not as a speculative optimization.
FAQ
Is graphicsLayer the same as Canvas?
No. graphicsLayer transforms or composites existing composable content. Canvas is a composable drawing surface for custom geometry.
When should I use drawBehind instead of background?
Use background when it already describes the visual. Use drawBehind when the decoration needs custom DrawScope geometry, a custom stroke, or size-aware rendering.
Does drawWithCache cache forever?
No. Its cache is rebuilt when the drawing size or state read inside the cache block changes.
Summary
Use graphicsLayer for rendered-layer effects, drawBehind for decoration, drawWithContent for ordering, and drawWithCache for genuine drawing-resource reuse. Choose the narrowest API that matches the visual requirement, then measure before adding layers or caches for performance.