compose-shimmer is one modifier that makes a skeleton screen, if you put it in the right place
A simple shimmer library for Jetpack Compose.
At a glance
- What is it?
- compose-shimmer is a small Kotlin library for Jetpack Compose and Compose Multiplatform that gives you a shimmer modifier, a themable ShimmerTheme and a choice of animation bounds. The part that trips people up is not the effect but the modifier order, because only the content declared after the shimmer call is affected. The current release is 1.5.0, built against Compose 1.11.
- Who is it for?
- compose-shimmer fits an Android or Compose Multiplatform team that wants a loading placeholder that matches the platform look without hand writing an animation, and that can accept a small dependency. It is the wrong choice if your loading state needs to say something specific rather than shimmer, and it will not help if you put the modifier at the end of a chain where nothing follows it.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 105 days ago.
- What is it written in?
- Mainly Kotlin, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The whole library is a modifier and a theme
The project is described in one line as a library which offers a shimmering effect for Compose, and the composition of it is small enough to read in a minute.
There is a shimmer modifier, a ShimmerTheme, and a rememberShimmer function that produces a shimmer instance you can pass around. The quick start is to apply the shimmer modifier to any UI of your choice, and the README says the code will emit the shimmering UI.
@Composable
fun ShimmeringPlaceholder() {
Row(
modifier = Modifier
.shimmer() // <- Affects all subsequent UI elements
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(
modifier = Modifier
.size(80.dp, 80.dp)
.background(Color.LightGray),
)
}
}The typical use is a loading placeholder. You draw the shape of the content you are about to show, in a flat background colour, and the shimmer sweeps across it while the real data loads. The example is a row with a square and a column of two bars, which is the shape of an avatar next to a title and a subtitle.
The set of parameters is modest for a reason. The theme exposes simple configurations such as the shimmer's rotation or width, and a few unabstracted objects such as an AnimationSpec or a BlendMode. The README is honest that exposing those violates the principles of information hiding, and says that it allows for some great customizations, and points to the documentation in the data class itself and to the ThemingSamples in the sample app for the details.
Modifier order is the whole gotcha
The section on modifier placement is the part of the README that has the most practical value, because it explains a behaviour that otherwise looks like a bug.
As usual with modifiers, the order matters, and every UI component defined after the shimmer modifier will be affected by the animation, including child views and other modifiers. The first example demonstrates that only the inner Box is shimmering, even though the shimmer has been added to the outer Box's modifier stack, and the reason given is that the blue background is not shimmering, as it is sitting above the shimmer modifier.
Box(
modifier = Modifier
.size(128.dp)
.background(Color.Blue)
.shimmer(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(64.dp)
.background(Color.Red)
)
}If the modifier is applied earlier, the outer Box's background modifier is affected by the shimmer as well.
Box(
modifier = Modifier
.size(128.dp)
.shimmer()
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.size(64.dp)
.background(Color.Red)
)
}So the rule to remember is that the shimmer paints what comes after it in the chain, not what comes before, and a background set above the shimmer call is left alone. That is a sensible rule, since the effect works by compositing over the content that follows it, but it is the opposite of the intuition most people carry over from a background first, then content, layout arrangement.
Each view gets its own velocity unless you bound the animation
The default behaviour of the shimmer modifier is to create an animation which traverses over the view in a certain time, and the consequence is stated plainly: the animation will have a different velocity depending on the size of the view.
If you apply the modifier to multiple views, each of a different size, then each shimmer will have its own velocity. That is why the library offers a way to set the boundaries for the animation, through the ShimmerBounds option and the rememberShimmer function.
val shimmerInstance = rememberShimmer(shimmerBounds = ShimmerBounds.XXX)
Box(modifier = Modifier.shimmer(shimmerInstance))There are three choices. ShimmerBounds.View is the default, and the view's height and width will be used as the boundaries for the animation, which the README says is sufficient for most use cases. ShimmerBounds.Window uses the window's height and width coordinate system, so it creates a shimmer that travels over the whole window instead of only the views, and only views that have the shimmer modifier attached will be affected.
The Window option has a documented caveat that is worth reading before you use it. It might look odd on scrollable content, because the shimmer will be positioned relative to the window, so the shimmer will not be moved together with the content.
ShimmerBounds.Custom exists because of that downside. By using it, the shimmer and its content will not be drawn until the bounds are set manually, using the updateBounds method on the Shimmer.
That last option is the price of full control. A shimmer that appears only once you have told it how big the area is cannot be used for an unbounded layout without a second pass, which is a real constraint in a list that grows as it loads.
Theming is a composition local, not a parameter on every call
The library includes a ShimmerTheme which can be provided as a local composition. The suggested practice is to integrate the theming into your customized MaterialTheme, and the README makes the point that there is no need to wrap every single shimmer into a CompositionLocalProvider.
val yourShimmerTheme = defaultShimmerTheme.copy(/* Set your desirable values */)
CompositionLocalProvider(
LocalShimmerTheme provides yourShimmerTheme
) {
/* content */
}The alternative to that is to pass the theme as a parameter using the rememberShimmer function, which is explained further down the README.
So there are two routes to a custom look. The composition local route is the one for an application, where the theme is set once near the root and every shimmer in the app inherits it. The parameter route is the one for a screen or a component that needs to look different from the rest of the app.
That is a small design decision with a large effect on how the library feels in a real project. A shimmer that has to be themed at every call site ends up with an inconsistent look the first time someone forgets one, and a shimmer that follows the app theme cannot be a one off without opting out of the local. Both are available here, and the default in the code samples is the local, because the common case is that every placeholder in the app should look the same.
One version number has to match your Compose version
The library is published to Maven Central, and the dependency is a single line.
dependencies {
implementation("com.valentinilk.shimmer:compose-shimmer:1.5.0")
}The supported Compose Multiplatform targets are listed in the README: Android, iOS, JVM on Desktop, JS in the browser and Wasm on WebAssembly. That list is what makes this a Compose Multiplatform library rather than an Android one, and the sample directory in the repository, along with the iosApp/ and kotlin-js-store/ directories, shows the multiplatform shape of the project.
The compatibility rule is the part to check before you add the dependency. In case of compatibility issues, select a library version that is based on the same Compose version as your project, and the README gives the mapping.
| Shimmer Version | Based on Compose | |-----------------|------------------| | 1.5.0 | 1.11 | | 1.4.0 | 1.10.3 | | 1.3.3 | 1.8.1 | | 1.3.2 | 1.7.3 |
This is a real constraint rather than a formality, because the library reaches into Compose's rendering internals to draw the effect. A shimmer built against a different Compose version is not guaranteed to behave, and the mapping table is the project's own guidance on which combination was tested.
On the releases, v1.5.0 was published on 2026-07-09, preceded by v1.5.0-beta02 and v1.4.0 on 2026-04-14, and the last push was on the same day as the 1.5.0 release. The project is not archived, and the licence is Apache 2.0, with the licence text in LICENSE.md. The documentation is generated with MkDocs, given the mkdocs.yml file at the root, and published at valentinilk.github.io/compose-shimmer.
Editorial conclusion
compose-shimmer fits an Android or Compose Multiplatform team that wants a loading placeholder that matches the platform look without hand writing an animation, and that can accept a small dependency. It is the wrong choice if your loading state needs to say something specific rather than shimmer, and it will not help if you put the modifier at the end of a chain where nothing follows it. Verify first that the library version you take matches your Compose version, using the table in the README, and check where in your modifier chain the shimmer call lands before you write the placeholder around it.
Frequently asked questions
How do I add compose-shimmer to a project?
The library is on Maven Central, so add implementation("com.valentinilk.shimmer:compose-shimmer:1.5.0") to your dependencies. Supported Compose Multiplatform targets are Android, iOS, JVM on Desktop, JS in the browser and Wasm on WebAssembly.
Why is part of my layout not shimmering?
Because the modifier order decides it. Every UI component defined after the shimmer modifier is affected by the animation, and a background set above the shimmer call in the chain is not, so a modifier placed after .background() leaves that background untouched.
What are the ShimmerBounds options?
ShimmerBounds.View, the default, uses the view's own height and width as the animation boundaries. ShimmerBounds.Window uses the window's coordinates so the shimmer travels over the whole window, and the README warns it can look odd on scrollable content. ShimmerBounds.Custom draws nothing until the bounds are set manually with the updateBounds method.
How do I theme the shimmer?
Provide a ShimmerTheme as a local composition, typically by integrating it into your customized MaterialTheme, so every shimmer in the app inherits it. The alternative is to pass the theme as a parameter to the rememberShimmer function, and the theme exposes simple settings such as rotation and width plus unabstracted values like AnimationSpec and BlendMode.
Which compose-shimmer version should I use?
Pick the library version built on the same Compose version as your project. The README maps 1.5.0 to Compose 1.11, 1.4.0 to 1.10.3, 1.3.3 to 1.8.1 and 1.3.2 to 1.7.3. The current release is v1.5.0 from 2026-07-09 and the licence is Apache 2.0.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/valentinilk-compose-shimmer)Was this useful?