Skip to content

onthecrow/nimbus

Repository files navigation

Nimbus

Nimbus

License Maven Central Multiplatform Telegram

An animated ring of glowing spikes for any Compose Multiplatform composable.


Showcase

GLOW HOLO BLUR
GLOW effect HOLO effect BLUR effect
Circle Rounded rectangle Rectangle

What it is

Nimbus draws a ring of animated spikes around a composable — a slow, breathing halo that grows out of the shape's own outline. Spikes fade in and out at zero height, drift or rotate, and pulse in size, so the ring never looks mechanical.

It renders outside the composable's bounds and changes neither its size nor the layout around it, so it can be attached to a button that already exists without touching anything else.

A backend is picked automatically and the result looks the same on all of them:

Target Backend
iOS, Desktop, Web Skia SkSL runtime shader
Android 33+ AGSL runtime shader
Android 29–32 off-screen GLSL, zero-copy via HardwareBuffer
Android 26–28 off-screen GLSL via glReadPixels
fallback Canvas, geometry only

The shader body is shared verbatim across all three dialects, so the backends cannot drift apart. While the effect is off the node is not redrawn at all, and while the app is backgrounded the frame loop suspends — a disabled Nimbus costs nothing.

Supported platforms

Platform Support Notes
Android minSdk 26
iOS — device (arm64)
iOS — simulator (Apple Silicon) iosSimulatorArm64
iOS — simulator (Intel) not supported by Compose itself, see below
Desktop (JVM)
Wasm
JS

Important

iosX64 — the Intel simulator — is not available. This is not a Nimbus decision: Compose Multiplatform does not publish an iosX64 variant at all, and never has. Any Compose Multiplatform library is in the same position.

So if your project declares iosX64(), it already cannot depend on Compose there. Drop the target — Apple Silicon simulators use iosSimulatorArm64 — or keep Compose and Nimbus out of that source set.

Download

Nimbus is on Maven Central, so no extra repository is needed.

Kotlin Multiplatform
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("io.github.onthecrow:nimbus:0.1.1")
        }
    }
}
Android only
dependencies {
    implementation("io.github.onthecrow:nimbus:0.1.1")
}

Gradle module metadata resolves the right variant automatically, so a plain Android project depends on the same coordinate.

Version catalog
[versions]
nimbus = "0.1.1"

[libraries]
nimbus = { module = "io.github.onthecrow:nimbus", version.ref = "nimbus" }

The badge above always shows the latest released version.

Quick start

One modifier, applied to a composable you already have:

val shape = CircleShape
var active by remember { mutableStateOf(false) }

Button(
    onClick = { active = !active },
    shape = shape,
    modifier = Modifier.nimbus(shape = shape, active = active),
) {
    Text("Connect")
}

Two things are worth knowing straight away:

  • Pass the composable's own shape. The spikes are built on that outline; a mismatch makes them grow from the wrong edge.
  • Nothing above it may clip. The effect deliberately draws past its own bounds, so a Modifier.clip(), a snug Card, or a LazyColumn item boundary will cut it off. Leave room around the composable — roughly maxPeakHeight × min(width, height) / 2 + collar on every side.

Customization

Modifier.nimbus

Parameter Default What it does
shape RectangleShape Outline the spikes are built around. Match the composable's own shape.
active true Shows or hides the effect, animating over activationSpec instead of popping.
layers two built-in layers The spike rings, drawn back to front.
sharpness 0.5f 0f wide rounded domes, 1f narrow drawn-out points.
collar 4.dp Band joining the spikes at their base. 0.dp leaves them separate.
effect GLOW GLOW, BLUR or HOLO.
effectIntensity 1f 0f leaves the bare spike geometry.
countEasing FastOutSlowInEasing Easing while a layer's spike count sweeps.
rotationEasing LinearEasing Easing within one revolution of a rotating layer.
activationSpec tween(600) Animation used when active flips.

NimbusLayer

Sizes are fractions of the reference sizemin(width, height) / 2, the radius of the largest circle fitting inside the composable. On a 120.dp circle that is 60.dp.

Parameter Default What it does
color Colour of this layer's spikes.
minSpikesCount 4 Lower bound of the count. Equal to max keeps it fixed.
maxSpikesCount 10 Upper bound. Capped at 20 by the shader's loop limit.
maxPeakHeight 0.4f How far a spike reaches past the edge, as a fraction of the reference size.
alpha 0.85f Opacity of the layer.
spawnSpeed 0.10f Sweeps per second between min and max count. 0f freezes it.
sizeChangeSpeed 1.2f Rad/s of each spike's own height pulsation.
motionMode RIGID RIGID turns the ring as a whole, INDEPENDENT lets each spike drift.
rotationSpeed 0.2f Rad/s of the ring; the sign sets direction. RIGID only.
wobbleAmount 0.30f How far a spike strays from its slot. INDEPENDENT only.
wobbleSpeed 0.5f Rad/s of that drift. INDEPENDENT only.
jitter 0.12f Static irregularity, so the ring is not machine-perfect.
widthFactor 1.15f Spike width relative to the gap. Above 1f they merge into a ridge.

Layered example

Depth comes from stacking rings that turn at different speeds and directions:

Modifier.nimbus(
    shape = shape,
    active = active,
    effect = NimbusEffect.GLOW,
    layers = listOf(
        NimbusLayer(
            color = Color(0xFF4DD0E1),
            minSpikesCount = 5,
            maxSpikesCount = 9,
            maxPeakHeight = 0.12f,
            alpha = 0.80f,
            rotationSpeed = 0.20f,
        ),
        NimbusLayer(
            color = Color(0xFF7C4DFF),
            minSpikesCount = 4,
            maxSpikesCount = 7,
            maxPeakHeight = 0.20f,
            alpha = 0.75f,
            rotationSpeed = -0.15f,
        ),
    ),
)

Sample app

Nimbus sample app

The sample is a tuning bench rather than a demo: every parameter of the public API is wired to a control, so you can dial the effect in on a real device instead of guessing values in code.

  • The button sits at the top and updates live as you change anything.
  • The Main tab holds shape, effect, collar, easings, activation and colours.
  • One tab per layer, with + to add a layer and × to remove one, so you can build up a multi-ring look and watch it develop.
  • Settings survive a restart, and Reset to defaults brings back the tuned baseline.

Export what you tuned

The button in the top-right corner generates a NimbusButton.kt with your current settings baked in — a complete, compilable composable, not a config dump — and opens the system share sheet so you can send it to yourself. Drop it into a project, add the dependency, and call NimbusButton().

Running it

# Android
./gradlew :androidApp:installDebug

# Desktop (macOS, Windows, Linux)
./gradlew :desktopApp:run

For iOS, open iosApp/ in Xcode and run from there.

The desktop build is the quickest way to see the Skia backend: Compose renders through Skia on Desktop, iOS and Web alike, so :desktopApp:run exercises the same shader path as iOS without needing a device or Xcode.

Contributing

Issues and pull requests are welcome. If you are changing how the effect looks, please say which backend you tested on — the Android GL fallbacks below API 33 are easy to break without noticing, since most devices never take that path.

Questions or ideas: @onthecrow on Telegram.

License

Copyright 2026 Dmitry Voronov

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages