Alongside this years Google I/O announcements, Material 3 Expressive was released to offer a way for apps to become more individual and break out of the constraints that the original Material Design principles had created. With Material 3 expressive, our apps not only get the chance to stand out from one another, but also allow use to create more engaging and expressive experiences. In this series of blog posts I’ll be diving into each component and sharing how you can get them into your apps.
In this first post we’ll be taking a look at the LoadingIndicator, this component allows you to show a Material Design Progress Indicator inside of your UI.
@Composable
fun LoadingIndicator(
modifier: Modifier = Modifier,
color: Color = LoadingIndicatorDefaults.indicatorColor,
polygons: List<RoundedPolygon> = LoadingIndicatorDefaults.IndeterminateIndicatorPolygons,
)
As we can see, this composable is quite minimal which is to be expected as there isn’t much we’ll need to modify when composing a progress indicator. All of the 3 arguments for the composable are optional, so we can simply compose the LoadingIndicator in an argument-less fashion:
LoadingIndicator()
When this is composed, we can see the LoadingIndicator animating between several different shapes:

This is very different from any of the Progress Indicator composables that we may be used to, but the naming is explicit in this. While the Progress indicator implies the indication of a current progress state, a loading state implies this infinite motion without the communication of progress. While this may not always give a clear indication of where things are currently at in regards to progress, it keeps things much more engaging while an application is in a loading state.
There is an additional LoadingIndicator composable that allows you to control the progress of the shapes, but this is more for controlling the animation of the progress as opposed to indicating the current progress.
@Composable
fun LoadingIndicator(
progress: () -> Float,
modifier: Modifier = Modifier,
color: Color = LoadingIndicatorDefaults.indicatorColor,
polygons: List<RoundedPolygon> = LoadingIndicatorDefaults.DeterminateIndicatorPolygons
)
Going back to the initial composable that we looked at, we can apply some modifications to the LoadingIndicator if we want to configure how it is displayed within our apps. The most obvious property within the list of arguments is the color – by default this will be styled using the primary color of your application, but the color argument can be used to modify the color of the loading indicator.
LoadingIndicator(
color = Color.Red
)

Alongside styling the color of the loading indicator we can also control the shapes that the indicator should morph between across the different animation states. This is done using the polygons argument, providing a list of RoundedPolygon instances that will be animated between. When doing this we must provide a minimum of two polygons in the list, otherwise your app will crash – this is because the loading indicator needs more than one polygon so that it can animate between shapes.
val size = 200.dp.value
val square = RoundedPolygon(
numVertices = 4,
radius = size / 2f,
centerX = size / 2f,
centerY = size / 2f
)
val roundedSquare = RoundedPolygon(
numVertices = 4,
radius = size / 1.5f,
centerX = size / 2f,
centerY = size / 2f,
rounding = CornerRounding(
size / 5f,
smoothing = 0.1f
)
)
LoadingIndicator(
polygons = listOf(
square,
roundedSquare
)
)
With this in place, we can see our loading indicator now animating between these two different polygons.

While this is a simple example, it shows how we can break out of the default offering of the Loading Indicator that is applied when composing the LoadingIndicator – which is something a lot of apps may just use out of the box. Here I have only used two shapes, but we can go a step further here and provide some additional shapes to be animated between:
val size = 200.dp.value
val square = RoundedPolygon(
numVertices = 4,
radius = size / 2f,
centerX = size / 2f,
centerY = size / 2f
)
val roundedSquare = RoundedPolygon(
numVertices = 4,
radius = size / 1.5f,
centerX = size / 2f,
centerY = size / 2f,
rounding = CornerRounding(
size / 5f,
smoothing = 0.1f
)
)
val oct = RoundedPolygon(
numVertices = 8,
radius = size / 1.5f,
centerX = size / 2f,
centerY = size / 2f,
rounding = CornerRounding(
size / 5f,
smoothing = 0.1f
)
)
val star = RoundedPolygon.Companion.star(
numVerticesPerRadius = 6,
)
LoadingIndicator(
color = MaterialTheme.colorScheme.primary,
polygons = listOf(
square,
roundedSquare,
oct,
star
)
)

While we are again using some simple shapes to add expression here, polygons could be created that closer represent your apps (or company) branding – this allows you to add a unique touch to the LoadingIndicator which helps it to fit in more with the look and feel of your app.
Outside of this LoadingIndicator composable, there is also a corresponding ContainedLoadingIndicator composable that allows you to display this indicator within a filled container:
@Composable
fun ContainedLoadingIndicator(
modifier: Modifier = Modifier,
containerColor: Color = LoadingIndicatorDefaults.containedContainerColor,
indicatorColor: Color = LoadingIndicatorDefaults.containedIndicatorColor,
containerShape: Shape = LoadingIndicatorDefaults.containerShape,
polygons: List<RoundedPolygon> = LoadingIndicatorDefaults.IndeterminateIndicatorPolygons,
)
This variant is useful in cases where you may be looking to apply more emphasis to the indicator, which may depend on where it is being shown within your UI. The Loading Indicator inside of this container works in the same way as the independent LoadingIndicator composable, using default argument values to display the different shapes. Just like the LoadingIndicator composable, you can also override this to customise the look and feel of the loading animation:
ContainedLoadingIndicator(
polygons = listOf(
square,
triangle,
oct,
star
)
)

With the main difference being the container of the Loading Indicator, we may want to apply styling to theme the container (and the indicator itself) to better fit with where it is positioned with our UI. These arguments have default values and will follow the theming of our application if not provided, but
ContainedLoadingIndicator(
containerColor = MaterialTheme.colorScheme.primary,
indicatorColor = Color.White,
containerShape = RoundedCornerShape(12.dp),
polygons = listOf(
square,
triangle,
oct,
star
)
)

In this post we’ve learnt about both the LoadingIndicator and ContainedLoadingIndicator composables which are available now as part of the Material 3 Expressive packages. We can see how these loading indicators provide more expression for your app while also giving the opportunity for more unique branding within your UI. This move to an expressive material style will helps apps to stand out more form one another, while also providing a more engaging experience for users – in this case, specifically while waiting to transition between loading states. Users will no longer see the uninteresting spinners that we are used to, but be taken through the state of progress via animated shapes. Stay tuned for more posts on Material 3 expressive components coming soon!