
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.
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:
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.
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:
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.
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:
| Parameter | What it controls on Jetpack canvas |
|---|---|
| width | thickness of the stroke line |
| cap | shape of the line ends: Butt, Round, or Square |
| join | shape of corners: Miter, Round, or Bevel |
| miter | limit on how sharp a Miter join can be |
| pathEffect | optional pattern, such as dashed lines |
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.
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:
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.
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:
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.
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:
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.
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:
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.
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.
A few smaller tools round out the Jetpack Compose Canvas toolkit, and Jetpack expects less frequent use than the core drawing functions above.
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.
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.
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.
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.
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.