Real-time native backdrop blur for Compose Multiplatform (Android + iOS).
Blurs whatever is behind it in the view hierarchy — like iOS UIVisualEffectView or CSS backdrop-filter, but cross-platform with a single Compose API.
- True backdrop blur — captures and blurs live content behind the overlay, not its own children
- Uniform blur — constant radius across the entire surface
- Variable blur — per-pixel radius controlled by linear or radial gradients
- Blend modes — 12 blend modes including Color Dodge, Multiply, Screen, Overlay
- Color tint — tint the blurred content with any color + blend mode
- Native GPU performance — OpenGL Dual Kawase on Android, CABackdropLayer on iOS
| Android | iOS | |
|---|---|---|
| Blur engine | OpenGL ES 2.0 Dual Kawase | CABackdropLayer (GPU compositor) |
| Min version | API 24 | iOS 15 |
| Published targets | Android release | iosArm64, iosSimulatorArm64 |
| Variable blur | OpenGL pyramid compositing | CAFilter variableBlur + mask |
| Performance | ~39 texture samples/pixel | Zero-cost compositor capture |
Android captures non-protected TextureView and SurfaceView content without
using the platform blur API. On API 24–35, overlapping SurfaceViews with a
custom compositor sublayer should call
registerBlurCaptureCompositionOrder(order); API 36+ discovers the order
directly. Secure and DRM surfaces cannot be captured.
Add the dependency to your KMP module:
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.ezoushen:blur-cmp:<version>")
}
}
}The blur engine is bundled — no additional dependencies needed.
Blurs whatever is behind it. Place it on top of any content:
@Composable
fun MyScreen() {
val blurState = rememberBlurOverlayState(
initialConfig = BlurOverlayConfig(radius = 20f)
)
Box(Modifier.fillMaxSize()) {
// Your scene — this gets blurred
MyContent()
// Blur overlay — blurs everything behind it
BlurOverlay(state = blurState) {
// Sharp controls on top
Text("Hello", color = Color.White)
}
}
}If you want the blur composable to manage both background and foreground:
@Composable
fun MyScreen() {
val blurState = rememberBlurOverlayState()
BlurOverlayHost(
state = blurState,
background = { PhotoGallery() },
content = { OverlayControls() },
)
}BlurOverlayConfig(
radius = 20f, // blur radius in logical pixels (0 = no blur)
tintBlendMode = BlurBlendMode.Normal, // blend mode for tint
tintOrder = TintOrder.POST_BLUR, // POST_BLUR (default) or PRE_BLUR
downsampleFactor = 4f, // Android only: higher = faster, lower quality
gradient = null, // null = uniform blur, or BlurGradientType
isLive = true, // true = updates every frame
)BlurOverlayConfig.Default // radius 16, no tint
BlurOverlayConfig.Light // radius 10, white tint 25%
BlurOverlayConfig.Dark // radius 20, black tint 40%
BlurOverlayConfig.Heavy // radius 50, white tint 50%Use the withTint extension to set a Compose Color:
val config = BlurOverlayConfig(radius = 20f)
.withTint(Color.White.copy(alpha = 0.2f))Read it back:
val tintColor: Color? = config.tintColorVariable blur lets the blur intensity vary across the surface using a gradient.
// Top-to-bottom: full blur at top, clear at bottom
BlurOverlayConfig(
radius = 30f,
gradient = BlurGradientType.Linear(
startX = 0.5f, startY = 0f, // top center
endX = 0.5f, endY = 1f, // bottom center
startIntensity = 1f, // full blur
endIntensity = 0f, // no blur
),
)
// Convenience factory
BlurOverlayConfig(
radius = 30f,
gradient = BlurGradientType.verticalTopToBottom(),
)// Sharp center, blurred edges
BlurOverlayConfig(
radius = 25f,
gradient = BlurGradientType.Radial(
centerX = 0.5f, centerY = 0.4f,
radius = 0.4f,
centerIntensity = 0f, // sharp
edgeIntensity = 1f, // blurred
),
)
// Convenience factory
BlurOverlayConfig(
radius = 25f,
gradient = BlurGradientType.spotlight(centerX = 0.5f, centerY = 0.4f, radius = 0.4f),
)BlurOverlayConfig(
radius = 30f,
gradient = BlurGradientType.Linear(
startX = 0.5f, startY = 0f,
endX = 0.5f, endY = 1f,
stops = listOf(
BlurGradientType.Stop(0.0f, 1.0f), // full blur at top
BlurGradientType.Stop(0.3f, 0.0f), // clear zone
BlurGradientType.Stop(0.7f, 0.0f), // clear zone
BlurGradientType.Stop(1.0f, 1.0f), // full blur at bottom
),
),
)12 blend modes for tint compositing:
BlurOverlayConfig(
radius = 15f,
tintBlendMode = BlurBlendMode.ColorDodge,
).withTint(Color.White.copy(alpha = 0.2f))Available modes: Normal, ColorDodge, ColorBurn, Multiply, Screen, Overlay, SoftLight, HardLight, Darken, Lighten, Difference, Exclusion
Color Dodge with tint creates a brightening bloom effect.
By default, tint is applied after blur (TintOrder.POST_BLUR), matching Apple's UIVisualEffectView and CSS backdrop-filter. This produces a sharp, uniform tint over the blurred result.
For a softer look where the tint gets diffused by the blur, use TintOrder.PRE_BLUR:
BlurOverlayConfig(
radius = 15f,
tintBlendMode = BlurBlendMode.ColorDodge,
tintOrder = TintOrder.PRE_BLUR, // tint blended into content before blur
).withTint(Color.White.copy(alpha = 0.2f))val blurState = rememberBlurOverlayState()
// Update config dynamically
blurState.config = BlurOverlayConfig(radius = newRadius)
// Toggle blur on/off
blurState.isEnabled = false
// Convenience setters
blurState.setRadius(25f)
blurState.setTintColor(Color.Blue.copy(alpha = 0.1f))
blurState.setGradient(BlurGradientType.spotlight())
// Force update when isLive = false
blurState.requestUpdate()Uses BlurView / VariableBlurView hosted via AndroidView:
- Capture: SurfaceTexture GPU capture via
lockHardwareCanvas(API 26+), or software canvas fallback - Blur: OpenGL ES 2.0 Dual Kawase with shared downsample chain (~5ms on Pixel 9)
- Output:
glReadPixels→canvas.drawBitmap, or TextureView for TBDR GPUs - Content exclusion: overlay content hidden during capture to prevent glow artifacts
- API 31+:
RenderNodeBlurControllerusesRenderEffectfor uniform blur (zero-copy GPU path)
Backdrop BlurOverlays always use the custom Kawase path. Each simultaneous
sibling or nested modal overlay owns a transparent edge-to-edge dialog and
composites every lower window from the activity upward, including each lower
layer's blur and sharp UI. Each layer keeps its own live/frozen update policy.
BlurOverlay and BackdropBlurDialog register their hosting windows automatically.
Call RegisterBackdropCaptureSource only for a source-only custom dialog or sheet
that must appear in blur overlays above it but does not contain its own BlurOverlay:
Dialog(onDismissRequest = onDismissRequest) {
RegisterBackdropCaptureSource()
SheetContent()
}Manual source injection remains available from an Android source set for hosts that cannot register from Compose:
CompositionLocalProvider(
LocalBlurOverlayPlatformContext provides BlurOverlayPlatformContext(
captureSources = listOf(
AndroidBlurOverlayCaptureSource(activity.window.decorView, activity.window),
AndroidBlurOverlayCaptureSource(sheetView.rootView, sheetWindow),
),
),
) {
BlurOverlay(state = blurState) { OverlayContent() }
}Sources are composited from back to front. Keep the list aligned with the currently mounted windows; an empty list falls back to the hosting activity.
Uses CABackdropLayer extracted from UIVisualEffectView:
CABackdropLayercaptures live window content at the GPU compositor level (zero-copy)CAFilter(gaussianBlur or variableBlur) applies blur natively- Blur overlay is added to
rootViewController.viewabove CMP's MetalView - Content renders in a separate transparent
UIWindowviaComposeUIViewController(opaque = false)
- Kotlin: 2.3.20+
- Compose Multiplatform: 1.11.0+
- Android Gradle Plugin: 8.13.2+ recommended for Kotlin 2.3 Android metadata
- Android: API 24+ (minSdk 24)
- iOS: 15+ on iosArm64 and iosSimulatorArm64
Compose Multiplatform 1.11.0 no longer publishes Apple x86_64 artifacts. Projects that need the Intel iOS simulator target must stay on the last 0.7.x blur-cmp line or maintain a separate legacy build.
Contributions are welcome! See CONTRIBUTING.md for guidelines.
- Bug reports — reproduce and fix issues
- Feature requests — suggest improvements
- Pull requests — code contributions
See CHANGELOG.md for version history.
Apache License 2.0