Skip to content

Blur Effects

miuix-blur is a standalone blur effect library for Compose Multiplatform. It provides backdrop blur, color blending, and texture effects via Modifier extensions. The library supports Android, Desktop (JVM), iOS, macOS, and Web (WasmJs/Js).

WARNING

On Android, blur effects require API 31 (Android 12) or higher. Custom blend modes via RuntimeShader require API 33 (Android 13) or higher. On unsupported API levels, blur modifiers become no-ops.

Setup

Add the miuix-blur dependency to your project:

kotlin
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("top.yukonga.miuix.kmp:miuix-blur:<version>")
        }
    }
}

For Android-only projects:

kotlin
dependencies {
    implementation("top.yukonga.miuix.kmp:miuix-blur-android:<version>")
}

Platform Support

PlatformRenderEffect (Blur)RuntimeShader (Custom Blend Modes)
AndroidAPI 31+API 33+
Desktop (JVM)SupportedSupported
iOS / macOSSupportedSupported
WasmJs / JsSupportedSupported

You can check platform capabilities at runtime:

kotlin
import top.yukonga.miuix.kmp.blur.isRenderEffectSupported
import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported

val canBlur = isRenderEffectSupported()
val canUseCustomBlendModes = isRuntimeShaderSupported()

Basic Usage

Applying a backdrop blur involves three steps:

  1. Create a LayerBackdrop to capture the background content
  2. Apply Modifier.layerBackdrop() on the content container
  3. Apply Modifier.textureBlur() on the blur surface
kotlin
import top.yukonga.miuix.kmp.blur.BlurColors
import top.yukonga.miuix.kmp.blur.BlurDefaults
import top.yukonga.miuix.kmp.blur.layerBackdrop
import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop
import top.yukonga.miuix.kmp.blur.textureBlur

// Step 1: Create a LayerBackdrop
val backdrop = rememberLayerBackdrop()

// Step 2: Capture background content
Box(
    modifier = Modifier
        .fillMaxSize()
        .layerBackdrop(backdrop) // Captures this Box's content
) {
    // Background content (e.g., an image, gradient, or page content)
    Image(
        painter = painterResource(Res.drawable.background),
        contentDescription = null,
        modifier = Modifier.fillMaxSize(),
        contentScale = ContentScale.Crop
    )
}

Background Color

layerBackdrop only captures the content drawn by the composable it is applied to — it does not include backgrounds from parent composables (e.g., Scaffold's Surface). If the captured content has transparent areas (such as text without a background), the blur will spread colors into transparency, producing visible color artifacts.

To avoid this, draw an opaque background in the onDraw lambda:

kotlin
val backgroundColor = MaterialTheme.colorScheme.surface
val backdrop = rememberLayerBackdrop {
    drawRect(backgroundColor) // Ensures an opaque background is captured
    drawContent()
}
kotlin

// Step 3: Apply blur on an overlay surface
Box(
    modifier = Modifier
        .size(200.dp)
        .textureBlur(
            backdrop = backdrop,
            shape = RoundedCornerShape(16.dp)
        )
) {
    Text(
        text = "Blurred Card",
        modifier = Modifier.padding(16.dp)
    )
}

Color Configuration

Use BlurColors to apply color adjustments and blend layers on top of the blur:

kotlin
import top.yukonga.miuix.kmp.blur.BlendColorEntry
import top.yukonga.miuix.kmp.blur.BlurBlendMode
import top.yukonga.miuix.kmp.blur.BlurColors

val colors = BlurColors(
    blendColors = listOf(
        BlendColorEntry(
            color = Color.White.copy(alpha = 0.3f),
            mode = BlurBlendMode.SrcOver
        )
    ),
    brightness = 0.05f,  // Range: [-1, 1], 0 = no change
    contrast = 1.1f,     // Multiplier, 1 = no change
    saturation = 1.2f    // Multiplier, 1 = no change
)

Box(
    modifier = Modifier
        .textureBlur(
            backdrop = backdrop,
            shape = RoundedCornerShape(16.dp),
            colors = colors
        )
) {
    // Content
}

You can also use the composable helper BlurDefaults.blurColors() which remembers the configuration:

kotlin
val colors = BlurDefaults.blurColors(
    blendColors = listOf(
        BlendColorEntry(Color.White.copy(alpha = 0.2f), BlurBlendMode.Screen)
    ),
    brightness = 0f,
    contrast = 1f,
    saturation = 1.2f
)

Blend Modes

BlurBlendMode provides 40+ blend modes for color blending over the blurred backdrop.

Standard Modes

Standard SkBlendMode values (0-28), using Skia-compatible premultiplied-alpha formulas:

ModeDescription
SrcOverNormal alpha compositing (default)
ScreenBrightening blend
MultiplyDarkening blend
OverlayContrast-enhancing blend
SoftLightSoft contrast adjustment
ColorDodgeBrightens by reducing contrast
ColorBurnDarkens by increasing contrast
DarkenKeeps darker pixels
LightenKeeps lighter pixels
DifferenceAbsolute difference
ExclusionSimilar to Difference but lower contrast
HueApplies source hue
SaturationApplies source saturation
ColorApplies source hue and saturation
LuminosityApplies source luminosity

Custom Modes

Custom modes (100+) implementing Lab color space operations, linear light blending, and more (requires isRuntimeShaderSupported()):

ModeDescription
LinearLightLinear light blend
LinearLightWithGreyscaleLinear light with greyscale modulation
LinearLightLabLinear light in Lab color space
LabLightenWithGreyscaleLab lighten with greyscale modulation
LabDarkenWithGreyscaleLab darken with greyscale modulation
LabLab color mapping
MiColorDodgeEnhanced color dodge
MiColorBurnEnhanced color burn
PlusDarkerPlus darker with alpha compositing
PlusLighterPlus lighter with alpha compositing
AlphaBlendAlpha blend with child modulation
MiSaturationSaturation adjustment
MiBrightnessBrightness adjustment
MiLuminanceLuminance curve adjustment

Example: Multiple Blend Layers

kotlin
val colors = BlurColors(
    blendColors = listOf(
        BlendColorEntry(Color(0x40FFFFFF), BlurBlendMode.Screen),
        BlendColorEntry(Color(0x20000000), BlurBlendMode.Overlay)
    ),
    saturation = 1.5f
)

Advanced Usage

Independent X/Y Blur Radii

Apply different blur strengths for horizontal and vertical axes:

kotlin
Box(
    modifier = Modifier
        .textureBlur(
            backdrop = backdrop,
            shape = RoundedCornerShape(16.dp),
            blurRadiusX = 100f,
            blurRadiusY = 20f
        )
) {
    // Content with directional blur
}

Foreground Blur (Content Masking)

Use contentBlendMode to create a foreground blur effect where the content's alpha channel masks the blur:

kotlin
import androidx.compose.ui.graphics.BlendMode as ComposeBlendMode

Text(
    text = "Frosted Text",
    style = MiuixTheme.textStyles.title1,
    modifier = Modifier
        .textureBlur(
            backdrop = backdrop,
            shape = RectangleShape,
            blurRadius = 150f,
            contentBlendMode = ComposeBlendMode.DstIn // Content alpha masks the blur
        )
)

Noise Dithering

The noiseCoefficient parameter controls anti-banding noise applied to the blur result. The default value is 0.0045f. Set to 0f to disable:

kotlin
Box(
    modifier = Modifier
        .textureBlur(
            backdrop = backdrop,
            shape = RoundedCornerShape(16.dp),
            noiseCoefficient = 0f // Disable noise dithering
        )
)

Low-Level API: drawBackdrop

For full control over the effect pipeline, use Modifier.drawBackdrop() with BackdropEffectScope:

kotlin
import top.yukonga.miuix.kmp.blur.drawBackdrop

Box(
    modifier = Modifier
        .drawBackdrop(
            backdrop = backdrop,
            shape = { RoundedCornerShape(16.dp) },
            effects = {
                // Apply a Blur
                blur(radius = 60f)
                // Adjust colors
                colorControls(
                    brightness = 0.05f,
                    contrast = 1.1f,
                    saturation = 1.3f
                )
            }
        )
) {
    // Content
}

BackdropEffectScope Extensions

ExtensionDescription
blur(radius, edgeTreatment)Applies a Blur
colorFilter(colorFilter)Applies a ColorFilter
colorControls(brightness, contrast, saturation)Adjusts brightness, contrast, and saturation
effect(effect)Chains an arbitrary RenderEffect
runtimeShaderEffect(key, shaderString, uniformShaderName, block)Applies a custom AGSL/SkSL runtime shader

Pixel-space uniforms must be scaled by downscaleFactor

When runtimeShaderEffect is chained after blur (or any other effect that raises downscaleFactor), the backdrop layer is recorded at 1 / downscaleFactor resolution and the shader receives coord values in the downscaled layer's pixel space. Any uniform that describes a pixel-space distance — size, padding/offset, corner radii, refraction band, etc. — must be divided by downscaleFactor inside block, otherwise samples land outside the layer bounds and return transparent black.

BackdropEffectScope Properties

PropertyTypeDescription
sizeSizeCurrent render size
layoutDirectionLayoutDirectionCurrent layout direction
shapeShapeCurrent clip shape
paddingFloatExtra padding for blur overflow
renderEffectRenderEffect?Accumulated effect chain
downscaleFactorIntDownsampling factor (1, 2, 4, 8, 16)
noiseCoefficientFloatNoise dithering coefficient for full-resolution application

Properties

textureBlur / textureEffect Parameters

Parameter NameTypeDescriptionDefault ValueRequired
backdropBackdropThe backdrop providing background content to blur-Yes
shapeShapeShape for the blur region clipping-Yes
blurRadiusFloatBlur radius in dp, internally converted to pixels using display density. Clamped to [0, 150]20fNo
blurRadiusXFloatHorizontal blur radius in dp (independent radii overload)-Yes*
blurRadiusYFloatVertical blur radius in dp (independent radii overload)-Yes*
noiseCoefficientFloatNoise dithering coefficient for anti-banding, 0 disables0.0045fNo
colorsBlurColorsColor adjustments and blend layers applied after blurBlurColors()No
highlightHighlight?Optional edge highlight painted on top of the content. null skips drawingnullNo
contentBlendModeBlendMode?Blend mode for compositing content over the blurnullNo
enabledBooleanWhether blur is active, when false the effect is skipped and content draws normallytrueNo

* Required only in the independent radii overload.

BlurColors Properties

Property NameTypeDescriptionDefault Value
blendColorsList<BlendColorEntry>Colors blended over the blurred backdrop, drawn in orderemptyList()
brightnessFloatBrightness adjustment in range [-1, 1]0f
contrastFloatContrast multiplier1f
saturationFloatSaturation multiplier1f

BlurDefaults

ConstantTypeDescriptionValue
BlurRadiusFloatDefault blur radius in dp20f
NoiseCoefficientFloatDefault noise dithering coefficient0.0045f
MaxBlurRadiusFloatMaximum allowed blur radius in dp150f
MethodReturn TypeDescription
blurColors()BlurColorsCreates a remembered BlurColors instance

Edge Highlight

A Highlight paints a thin glassy edge with two directional lights along a rounded shape. It is drawn through the highlight parameter on Modifier.textureBlur / Modifier.textureEffect (constant case) or on Modifier.drawBackdrop (reactive case, sharing the BackdropEffectScope). Combined with the blurred backdrop this produces a "lit edge over blurred backdrop" look.

With textureBlur (constant)

kotlin
import androidx.compose.foundation.shape.RoundedCornerShape
import top.yukonga.miuix.kmp.blur.highlight.Highlight

Box(
    modifier = Modifier
        .size(200.dp, 100.dp)
        .textureBlur(
            backdrop = backdrop,
            shape = RoundedCornerShape(24.dp),
            highlight = Highlight.GlassStrokeMiddleLight,
        ),
)

With drawBackdrop (reactive)

The highlight lambda runs inside the same BackdropEffectScope as effects, so its return value can change with state (e.g. press progress) and pick up the current size / shape automatically.

kotlin
import top.yukonga.miuix.kmp.blur.drawBackdrop
import top.yukonga.miuix.kmp.blur.blur
import top.yukonga.miuix.kmp.blur.highlight.Highlight

Box(
    modifier = Modifier
        .size(200.dp, 100.dp)
        .drawBackdrop(
            backdrop = backdrop,
            shape = { RoundedCornerShape(24.dp) },
            effects = { blur(20.dp.toPx()) },
            highlight = { Highlight.GlassStrokeMiddleLight.copy(alpha = pressProgress) },
        ),
)

Pass null (or a Highlight with width = 0.dp) to disable.

WARNING

Edge highlight requires isRuntimeShaderSupported(). On unsupported platforms or API levels, the highlight is skipped silently.

Built-in Tokens

Six presets are provided. Pick by card size and theme:

TokeninnerBlurRadiusVisual
Highlight.GlassStrokeBigLight3.5 dpThickest, softest halo — large light-mode cards
Highlight.GlassStrokeMiddleLight2.8 dpStandard light-mode card (default)
Highlight.GlassStrokeSmallLight2.6 dpCompact, sharper light-mode card
Highlight.GlassStrokeBigDark1.7 dpThinnest halo — large dark-mode card
Highlight.GlassStrokeMiddleDark2.0 dpStandard dark-mode card
Highlight.GlassStrokeSmallDark2.3 dpCompact, sharpest dark-mode card

Highlight.Default aliases GlassStrokeMiddleLight.

Custom BloomStroke

Override individual fields on a token, or build a BloomStroke from scratch:

kotlin
import top.yukonga.miuix.kmp.blur.highlight.BloomStroke
import top.yukonga.miuix.kmp.blur.highlight.LightPosition
import top.yukonga.miuix.kmp.blur.highlight.LightSource

val custom = Highlight(
    width = 1.dp,
    alpha = 0.8f,
    style = BloomStroke(
        color = Color.White.copy(alpha = 0.05f),
        innerBlurRadius = 3.dp,
        primaryLight = LightSource(
            position = LightPosition(0.5f, 0.4f, -0.5f),
            intensity = 0.4f,
        ),
        secondaryLight = LightSource(
            position = LightPosition(0.5f, 0.85f, -0.5f),
            intensity = 0.3f,
        ),
    ),
)

LightPosition is in normalized UV with the reference origin at (0.5, 0.7, 0). The shader normalizes (x − 0.5, y − 0.7, z) into a 3D unit direction. Negative z places the light behind the surface plane, illuminating the inward-facing edge — all built-in tokens use z < 0.

The two lights are restricted to opposite hemispheres by the shader: primaryLight lights the upper half of the rounded rect, secondaryLight lights the lower half. Position y < 0.7 biases a light upward; y > 0.7 biases it downward.

Sensor-driven Tilt Parallax

rememberTiltLight shifts a base position in real time using the device rotation sensor. On Android, this produces a parallax-like edge that follows device tilt; on Desktop / iOS / macOS / Web the tilt is always zero, so the lights stay anchored.

kotlin
import top.yukonga.miuix.kmp.blur.highlight.rememberTiltLight

val baseStyle = Highlight.GlassStrokeMiddleLight.style as BloomStroke

val tiltPrimary = rememberTiltLight(
    basePosition = baseStyle.primaryLight.position,
    intensity = baseStyle.primaryLight.intensity,
    sensitivity = 0.15f,
)
val tiltSecondary = rememberTiltLight(
    basePosition = baseStyle.secondaryLight.position,
    intensity = baseStyle.secondaryLight.intensity,
    sensitivity = 0.12f,
)

val highlight = Highlight.GlassStrokeMiddleLight.copy(
    style = baseStyle.copy(
        primaryLight = tiltPrimary,
        secondaryLight = tiltSecondary,
    ),
)

sensitivity controls how far the UV position shifts per radian of tilt — 0.1f shifts the light by 10% of the bounds at 1 rad. Higher values amplify the parallax.

Edge Highlight Properties

Highlight

PropertyTypeDescriptionDefault
widthDpStroke band width painted with style.color0.8.dp
alphaFloatOverall opacity multiplier, range [0, 1]1f
styleHighlightStyleShading model (typically a BloomStroke)HighlightStyle.Default

BloomStroke

PropertyTypeDescriptionDefault
colorColorBase color of the stroke band; alpha drives stroke brightnessWhite.copy(alpha = 0.05f)
blendModeBlendModeCompositing mode for the highlight overlayBlendMode.Plus
innerBlurRadiusDpDepth the lighting reaches inward from the rounded edge — controls halo softness and thickness2.8.dp
primaryLightLightSourceUpper-hemisphere lightLightPosition(0.5f, 0.5f, -0.5f), intensity 0.4
secondaryLightLightSourceLower-hemisphere lightLightPosition(0.5f, 0.8f, -0.5f), intensity 0.25

LightSource

PropertyTypeDescriptionDefault
positionLightPositionUV position of the light-
colorColorLight color (color alpha is folded into the contribution scale)White
intensityFloatBrightness scalar, ≥ 01f

LightPosition

PropertyTypeDescription
xFloatNormalized UV x (reference origin: 0.5)
yFloatNormalized UV y (reference origin: 0.7)
zFloatSigned depth; negative places the light behind the surface

rememberTiltLight Parameters

ParameterTypeDescriptionDefault
basePositionLightPositionPosition at zero tilt-
colorColorLight colorWhite
intensityFloatBrightness scalar1f
sensitivityFloatUV offset applied per radian of tilt0.1f

Changelog

Released under the Apache-2.0 License