Jetpack Compose Canvas

A Jetpack Compose Canvas is the composable Jetpack gives you whenever a design needs a custom shape, a progress ring, or a hand-built chart that no standard widget can produce. The Jetpack Canvas composable hands you a blank drawing surface and a set of drawing commands, so you paint exactly what the design calls for. Any time a screen needs custom graphics rather than a pre-built layout, the Jetpack Compose Canvas is the tool Jetpack built for that job.

This blog walks through every drawing function the Jetpack Compose Canvas exposes — shapes, stroke styles, color and brush, the coordinate system, transformations, custom paths, and the smaller helper tools Jetpack ships alongside it. Every code sample below is fully self-contained, so you can paste any of them into an online Kotlin compiler that supports Jetpack Compose and watch the Canvas composable behave exactly as Jetpack describes it.

What Is the Canvas Composable in Jetpack Compose

The Canvas composable is a function that Jetpack Compose provides to give you a raw drawing surface inside your layout. Unlike a Button or a Text composable, a Jetpack Compose Canvas does not render anything on its own — it exposes a lambda where you issue drawing instructions yourself. Jetpack calls this lambda's receiver DrawScope, and every drawing function on the Jetpack Compose Canvas lives on that receiver.

Here is the simplest Jetpack Compose Canvas example you will run into, showing the Jetpack Canvas composable at its most basic:

kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun BasicCanvasDemo() {
    Surface {
        Canvas(modifier = Modifier.size(150.dp)) {
            drawCircle(color = Color.Blue)
        }
    }
}

Running this Jetpack Compose Canvas example draws a filled blue circle that fits the available bounds, because the Canvas composable measures itself to 150dp by 150dp. Jetpack re-invokes this lambda on every recomposition, so a Jetpack Compose Canvas always stays in sync with whatever state drives your drawing. Find the full reference in the official Jetpack Compose graphics documentation.

Drawing Basic Shapes on the Jetpack Compose Canvas

Shape drawing is where most jetpack compose custom drawing work happens, and the Jetpack Compose Canvas ships a dedicated function for every common primitive. You can call several of these functions inside a single Canvas composable to build up a more complex picture, and Jetpack lets you freely mix them in any order.

These are the core shape functions Jetpack Compose Canvas provides:

  • drawLine — draws a straight line between two offsets on Jetpack canvas
  • drawRect — draws a rectangle at a given position and size on Jetpack canvas
  • drawRoundRect — draws a rectangle with rounded corners on Jetpack canvas
  • drawCircle — draws a circle around a center point on Jetpack canvas
  • drawOval — draws an oval inside a bounding rectangle on Jetpack canvas
  • drawArc — draws a curved arc segment, handy for gauges on a Jetpack Compose Canvas
  • drawPath — draws any custom Path object on Jetpack canvas
  • drawPoints — draws a series of individual points on Jetpack canvas
kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun ShapeGalleryCanvas() {
    Surface {
        Canvas(modifier = Modifier.size(220.dp)) {
            drawLine(
                color = Color.DarkGray,
                start = Offset(10f, 10f),
                end = Offset(200f, 10f),
                strokeWidth = 6f
            )
            drawRect(
                color = Color.Magenta,
                topLeft = Offset(10f, 30f),
                size = Size(80f, 60f)
            )
            drawCircle(
                color = Color.Green,
                radius = 40f,
                center = Offset(160f, 100f)
            )
            drawArc(
                color = Color.Red,
                startAngle = 0f,
                sweepAngle = 180f,
                useCenter = false,
                topLeft = Offset(10f, 150f),
                size = Size(120f, 60f)
            )
        }
    }
}

Running this Jetpack Compose Canvas example draws a line, a rectangle, a circle, and an arc together inside the same 220dp canvas, and Jetpack renders every shape in the order the drawing functions are called. Because each shape function on Jetpack canvas takes its own color, size, and position, combining shapes on a Jetpack Compose Canvas is mostly a matter of choosing the right offsets for each call.

Stroke and Fill Styles on the Compose Canvas

Every shape drawn on a Jetpack Compose Canvas can either be filled solid or drawn as an outline, and Jetpack controls this through a style parameter that accepts either Fill or a Stroke object. Choosing between them changes how the same shape renders on Jetpack canvas without changing anything else about the call.

Jetpack Stroke style on the Jetpack Compose Canvas takes several configuration values:

ParameterWhat it controls on Jetpack canvas
widththickness of the stroke line
capshape of the line ends: Butt, Round, or Square
joinshape of corners: Miter, Round, or Bevel
miterlimit on how sharp a Miter join can be
pathEffectoptional pattern, such as dashed lines
kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp

@Composable
fun StrokeStyleCanvas() {
    Surface {
        Canvas(modifier = Modifier.size(160.dp)) {
            drawCircle(
                color = Color.Blue,
                radius = 60f,
                center = Offset(80f, 80f),
                style = Stroke(width = 10f, cap = StrokeCap.Round)
            )
        }
    }
}

This Jetpack Compose Canvas example draws only the outline of a circle instead of a solid fill, because the style parameter passes a Stroke with a width of 10 pixels and rounded caps. Swapping that Stroke value for the default Fill would instantly turn the same circle solid, which shows how much control the style parameter gives you over any shape on a Jetpack Compose Canvas.

Adding Color and Brush to Your Canvas Drawing

Every drawing function on the Jetpack Compose Canvas accepts either a plain Color or a Brush, and Jetpack uses this pairing of paint and brush to decide how a shape gets filled. A solid color is the simplest option, but a Brush lets you paint gradients across a shape drawn on Jetpack canvas instead of a single flat tone.

Jetpack Compose ships several brush types that work with any drawing function on Jetpack canvas:

  • linearGradient — blends colors along a straight line across Jetpack canvas
  • radialGradient — blends colors outward from a center point on Jetpack canvas
  • sweepGradient — blends colors in a circular sweep on Jetpack canvas
  • horizontalGradient and verticalGradient — shorthand versions of linearGradient for Jetpack canvas
kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun GradientCanvasDemo() {
    Surface {
        Canvas(modifier = Modifier.size(180.dp)) {
            drawRect(
                brush = Brush.linearGradient(
                    colors = listOf(Color.Cyan, Color.Blue, Color.Magenta)
                )
            )
        }
    }
}

Here, drawRect fills the whole Jetpack Compose Canvas with a smooth blend from cyan to blue to magenta instead of a single color. The paint and brush combination makes gradients possible on a Jetpack Compose Canvas, since the Brush object defines how color changes across the shape. Read more about brush types in the Compose graphics color and brush guide.

Size, Center, and the Coordinate System of DrawScope

Every drawing function inside a Jetpack Compose Canvas operates on a coordinate system where the origin sits at the top-left corner of Jetpack canvas. Jetpack DrawScope exposes a size property telling you how many pixels wide and tall the surface is, essential for positioning shapes correctly on any Jetpack Compose Canvas.

A few properties make this coordinate system easy to work with on Jetpack canvas:

  • size — a Size object holding the width and height of the current Jetpack canvas
  • center — a convenient Offset representing the exact middle of Jetpack canvas
  • drawContext — gives access to lower-level canvas state, including size and transform
kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color

@Composable
fun CenteredCircleCanvas() {
    Surface {
        Canvas(modifier = Modifier.fillMaxSize()) {
            drawCircle(
                color = Color.Red,
                radius = size.minDimension / 4,
                center = center
            )
        }
    }
}

Because this Jetpack Compose Canvas example reads the size and center properties straight from DrawScope, the circle stays perfectly centered no matter how large Jetpack canvas becomes. That is the advantage of relying on Jetpack canvas's own coordinate system instead of hardcoding pixel values for every drawing surface.

Transforming the Jetpack Compose Canvas

Sometimes a drawing needs to move, rotate, or resize without recalculating every offset by hand, and the Jetpack Compose Canvas provides transformation functions that adjust the coordinate system for anything drawn inside them.

Jetpack exposes these transformation functions on Jetpack canvas:

  • translate — shifts the coordinate origin of Jetpack canvas by a horizontal and vertical amount
  • rotate — rotates everything drawn inside its block around a pivot point on Jetpack canvas
  • scale — resizes everything drawn inside its block by a horizontal and vertical factor
  • inset — shrinks the available drawing area of Jetpack canvas by a fixed margin
  • withTransform — combines multiple canvas transformations in a single block
kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun RotatedRectangleCanvas() {
    Surface {
        Canvas(modifier = Modifier.size(180.dp)) {
            rotate(degrees = 30f, pivot = center) {
                drawRect(
                    color = Color.Blue,
                    topLeft = Offset(50f, 70f),
                    size = Size(80f, 40f)
                )
            }
        }
    }
}

The rotate block in this Jetpack Compose Canvas example turns the rectangle by 30 degrees around the center before Jetpack draws it. Anything placed inside a transformation block on Jetpack canvas behaves as if the whole coordinate grid had been rotated, scaled, or shifted, saving you from recalculating every point on the Jetpack Compose Canvas.

Clipping and Custom Paths on the Canvas

Not every drawing on a Jetpack Compose Canvas needs to be a built-in shape. Jetpack Path class lets you describe any custom outline by chaining line and curve segments, and Jetpack canvas can then fill or stroke that path like a built-in shape — the usual approach for logos, icons, and unusual outlines.

Path building on Jetpack canvas relies on a handful of core commands:

  • moveTo — moves the starting point on Jetpack canvas without drawing anything
  • lineTo — draws a straight segment to the given point on Jetpack canvas
  • quadraticBezierTo — draws a curved segment using one control point
  • cubicTo — draws a curved segment using two control points
  • close — connects the last point back to the first point on the path

Alongside custom paths, the Jetpack Compose Canvas also supports clipRect and clipPath, which restrict drawing to a specific region of Jetpack canvas so nothing spills outside a boundary.

kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.unit.dp

@Composable
fun CustomPathCanvas() {
    Surface {
        Canvas(modifier = Modifier.size(160.dp)) {
            val trianglePath = Path().apply {
                moveTo(80f, 20f)
                lineTo(140f, 130f)
                lineTo(20f, 130f)
                close()
            }
            drawPath(path = trianglePath, color = Color.Green)
        }
    }
}

This Jetpack Compose Canvas example builds a triangle by moving to a starting point, drawing two lines, and closing the path back to the origin, then hands that Path straight to drawPath on Jetpack canvas. Combining custom paths with clipRect or clipPath gives you precise control over unusual shapes that the built-in canvas functions cannot produce alone.

Minor Canvas Tools: Native Canvas and Draw Modifiers

A few smaller tools round out the Jetpack Compose Canvas toolkit, and Jetpack expects less frequent use than the core drawing functions above.

  • nativeCanvas — reached through drawContext.canvas.nativeCanvas, gives access to the underlying platform canvas for operations DrawScope does not expose directly
  • drawIntoCanvas — a lower-level escape hatch that hands you the raw canvas object inside a lambda
  • Modifier.drawBehind — lets any composable draw on Jetpack canvas behind its own content without wrapping it in a separate Canvas composable
  • Modifier.drawWithContent — lets you draw on Jetpack canvas both before and after a composable's own content is drawn
kotlin
import androidx.compose.foundation.drawBehind
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Composable
fun DrawBehindDemo() {
    Surface {
        Box(
            modifier = Modifier
                .size(120.dp)
                .drawBehind {
                    drawCircle(color = Color.Yellow, radius = 50f, center = Offset(60f, 60f))
                }
        )
    }
}

This example uses drawBehind directly on a Box modifier, painting on Jetpack canvas underneath the Box without creating a standalone Jetpack Compose Canvas — a handy shortcut for a composable that only needs a small piece of custom graphics.

Practical Example: Building a Circular Progress Gauge

A common real-world use case for the Jetpack Compose Canvas is a custom progress gauge, since standard progress indicators rarely match a specific design. Combining drawArc, stroke styling, and the size property from DrawScope makes this straightforward on a Jetpack Compose Canvas.

The gauge below draws a light gray track on Jetpack canvas first, then layers a colored arc on top whose sweep angle represents the progress value. Because that sweep on Jetpack canvas comes directly from a progress fraction, updating the fraction redraws the gauge on the next recomposition.

kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.unit.dp

@Composable
fun ProgressGaugeCanvas(progress: Float) {
    Surface {
        Canvas(modifier = Modifier.size(140.dp)) {
            val strokeWidth = 14f
            drawArc(
                color = Color.LightGray,
                startAngle = -90f,
                sweepAngle = 360f,
                useCenter = false,
                style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
            )
            drawArc(
                color = Color.Blue,
                startAngle = -90f,
                sweepAngle = 360f * progress,
                useCenter = false,
                style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
            )
        }
    }
}

This gauge shows how a few drawing functions combine into a real interface element: an android jetpack compose canvas gauge like this one only needs two drawArc calls, a Stroke style, and a progress value on Jetpack canvas to produce a polished result. It is a solid template for volume sliders, loading indicators, or any dashboard widget built on a Jetpack Compose Canvas.

Complete Working Example

The full example below pulls together shape drawing, stroke styling, gradients, and a rotation transform in a single self-contained Jetpack Compose Canvas composable that you can paste directly into an online Kotlin compiler configured for Jetpack Compose.

kotlin
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp

@Composable
fun CompleteCanvasExample() {
    Surface(modifier = Modifier.fillMaxSize()) {
        Canvas(modifier = Modifier.size(200.dp)) {
            drawRect(
                brush = Brush.linearGradient(colors = listOf(Color.Cyan, Color.Blue)),
                size = Size(200f, 200f)
            )
            rotate(degrees = 20f, pivot = center) {
                drawRoundRect(
                    color = Color.White,
                    topLeft = Offset(40f, 40f),
                    size = Size(120f, 80f),
                    cornerRadius = CornerRadius(16f, 16f),
                    style = Stroke(width = 8f, cap = StrokeCap.Round)
                )
            }
            drawCircle(
                color = Color.Yellow,
                radius = 18f,
                center = center
            )
        }
    }
}

Every jetpack compose canvas example in this blog builds toward this final composable, which paints a gradient background on Jetpack canvas, rotates a rounded rectangle outline on top of it, and finishes with a small yellow circle centered on the Jetpack Compose Canvas. Running it in any Compose-enabled Kotlin environment renders all three layers on one drawing surface, with no setup beyond the imports already listed.