Custom Shapes
Every shape we've used so far — a Card's rounded corners, a chip's pill outline —
has come from a small, ready-made set. Compose exposes the underlying Shape
interface directly too, so any composable's silhouette can be clipped to something Material
doesn't ship out of the box. This closing lesson of the UI Components part covers the built-in
shapes plus GenericShape for fully custom outlines, applied to a small category
badge on the RecipeBox recipe card.
The Built-In Shapes
| Shape | Produces |
|---|---|
RectangleShape | Sharp, unrounded rectangle — the default for most containers |
RoundedCornerShape(radius) | Rectangle with rounded corners; also accepts a different radius per corner |
CircleShape | A perfect circle (or ellipse, on a non-square component) |
CutCornerShape(size) | Corners cut diagonally instead of rounded, for an angular look |
Modifier.clip: Applying a Shape
Any composable can be clipped to a shape with Modifier.clip(shape) — this is what
turned the square AsyncImage thumbnail in the Cards lesson into a rounded-corner
image, and the same modifier makes a perfectly circular avatar just as easily:
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
)
clip only affects drawing and hit-testing at the shape's boundary — it doesn't
change layout size, so combine it with Modifier.background(color, shape) (a single
call that both fills and clips together) when a shape needs a solid fill, as shown next.
Worked Example: A Custom Category Badge Shape
GenericShape builds an arbitrary outline by drawing a Path yourself,
given the composable's actual pixel Size at draw time. Here's a small
pentagon-ish "ribbon" badge shape for a recipe card's category label:
val RibbonBadgeShape = GenericShape { size, _ ->
val notch = size.height / 2f
moveTo(0f, 0f)
lineTo(size.width - notch, 0f)
lineTo(size.width, notch)
lineTo(size.width - notch, size.height)
lineTo(0f, size.height)
close()
}
The lambda's second parameter (ignored here with _) is a LayoutDirection,
useful if a shape needs to mirror for right-to-left locales; the first is the composable's
Size, letting the shape adapt to whatever width the badge ends up with rather than
hardcoding pixel values.
Applying the Badge to a Recipe's Category
Dropped onto a small colored box, the shape becomes an arrow-notched category badge that can
sit in the corner of the RecipeCard built two lessons ago:
@Composable
fun CategoryBadge(category: RecipeCategory) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clip(RibbonBadgeShape)
.background(category.color)
.padding(horizontal = 12.dp, vertical = 4.dp)
) {
Text(
text = category.label,
color = Color.White,
style = MaterialTheme.typography.labelSmall
)
}
}
Because Modifier.clip and Modifier.background are ordinary modifiers,
CategoryBadge composes into the corner of a RecipeCard's
Row exactly like any other small composable — no special integration needed beyond
picking where in the row to place it.
category.color above would actually come from in a real app, rather than a hardcoded value.