Blog Infos
Author
Published
Topics
, , , ,
Published
Easiest Way to Create a Shimmer Effect in Jetpack Compose

 

If you’ve ever scrolled through YouTube, LinkedIn, or any modern mobile app, you’ve seen the shimmer effect — a placeholder animation that gives users visual feedback while content loads. It’s subtle, smooth, and makes your app feel more responsive.

In Jetpack Compose, you don’t need a library or complicated setup to achieve this. All it takes is one Kotlin extension function. In this post, I’ll walk you through a simple Modifier.shimmer() extension that you can apply to any composable. You’ll also learn how to toggle shimmer using a loading state.

Creating a Shimmer Modifier

Here’s a custom extension you can apply to any composable to give it a shimmer effect:

@Composable
fun Modifier.shimmer(cornerRadius: Dp = 0.dp): Modifier {
    val shimmerColors = listOf(
        Color.LightGray.copy(alpha = 0.3f),
        Color.White.copy(alpha = 0.6f),
        Color.LightGray.copy(alpha = 0.3f)
    )

    val transition = rememberInfiniteTransition(label = "Shimmer")
    val translateAnim by transition.animateFloat(
        initialValue = -400f,
        targetValue = 1200f,
        animationSpec = infiniteRepeatable(
            animation = tween(
                durationMillis = 1600, // slower = smoother
                easing = FastOutSlowInEasing // smoother easing
            )
        ),
        label = "Translate"
    )

    return this.drawWithCache {
        val brush = Brush.linearGradient(
            colors = shimmerColors,
            start = Offset(translateAnim, 0f),
            // wider gradient
            end = Offset(translateAnim + size.width / 1.5f, size.height)
        )
        val cornerPx = cornerRadius.toPx()
        onDrawWithContent {
            drawRoundRect(
                brush = brush,
                cornerRadius = CornerRadius(cornerPx, cornerPx),
                size = size
            )
        }
    }
}

This modifier uses rememberInfiniteTransition() to animate a moving light across a gradient. The drawWithCache block is used for better performance since the brush can be reused. The result is a soft, moving light across your UI component, giving the appearance of loading.

Applying the Shimmer Effect

Let’s create a simple loading card using the modifier:

@Composable
fun LoadingCard() {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp)
            .padding(16.dp)
            .shimmer()
    )
}

You can use this on any UI component — ImageSurfaceBox, or even wrap entire rows or lists.

Making It Smarter with isLoading

Of course, we don’t want to apply a shimmer all the time. Let’s make it conditional with an isLoading flag.

First, extend the modifier like this:

@Composable
fun Modifier.shimmer(cornerRadius: Dp = 0.dp, isLoading: Boolean): Modifier {
    return if (isLoading) this.shimmer(cornerRadius = cornerRadius) else this
}

Now, in your composable, you can control when shimmer appears:

@Composable
fun ArticleCard(isLoading: Boolean) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp)
            .padding(16.dp)
            .shimmer(isLoading = isLoading, cornerRadius = 12.dp)
    ) {
        if (!isLoading) {
            Text(
                text = "Understanding Jetpack Compose State",
                modifier = Modifier.align(Alignment.Center)
            )
        }
    }
}

This is perfect when you’re loading data from an API or a ViewModel. As soon as the data is available, isLoading becomes false, and real content replaces the shimmer placeholder.

Reusable Wrapper Composable

You can also create a reusable composable to wrap any content in a shimmer effect. Here’s a simple ShimmerItem:

@Composable
fun ShimmerItem(isLoading: Boolean, content: @Composable () -> Unit) {
    Box(modifier = Modifier.shimmer(isLoading)) {
        if (!isLoading) content()
    }
}

Use it like this:

ShimmerItem(isLoading = true) {
    Text("Loaded text", modifier = Modifier.padding(16.dp))
}

This way, you can easily apply shimmer to any component in your UI tree without duplicating code.

Full Example with a List: Let’s put this all together in a LazyColumn:

@Composable
fun ArticleList(isLoading: Boolean) {
    LazyColumn(modifier = Modifier.fillMaxSize()) {
        items(5) {
            ArticleCard(isLoading = isLoading)
        }
    }
}

If isLoading is true, you’ll see shimmer cards. When the data is loaded, the real content will show up.

Customizing the Look

Want to make the shimmer effect match your brand? Just update the gradient colors. For example:

val shimmerColors = listOf(
    Color(0xFFE0E0E0),
    Color(0xFFBDBDBD),
    Color(0xFFE0E0E0)
)

Or make the animation vertical instead of diagonal by adjusting the gradient’s start and end offsets.

You can also add rounded corners to the shimmer by Just pass the desired Dp value to the cornerRadius parameter:

Modifier.shimmer(cornerRadius = 12.dp)

This makes it feel more like a card or a chip.

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Flutter: The Last UI Framework

As the Flutter framework continues to grow and change, it expands beyond what the team at Google can manage themselves. In this keynote, we’ll discuss how we continue to scale Flutter as a community, what…
Watch Video

Flutter: The Last UI Framework

Chris Sells
Flutter fanatic

Flutter: The Last UI Framework

Chris Sells
Flutter fanatic

Flutter: The Last UI Framework

Chris Sells
Flutter fanatic

Jobs

Final Result
Conclusion

Shimmer is one of those tiny UX details that makes a big difference. It shows users that your app is working, even when content isn’t ready. With Jetpack Compose, you don’t need to pull in an external library or create a custom layout — you can get it done with a few lines of Kotlin.

Whether you’re building a loading feed, a profile screen, or a settings page, this shimmer effect can make your app feel more polished and professional. And the best part? It’s reusable, customizable, and 100% Compose-native.

This article was previously published on proandroiddev.com.

Menu