
iOS 26–style frosted-glass surfaces for Compose: backdrop blur, chroma lift, edge sheen; quality tiers with graceful degradation, zero‑alloc fallback, dynamic sheen, grain and refraction.
iOS 26-style frosted glass surfaces for Compose Multiplatform. A Modifier.liquidGlass()
plus GlassCard, GlassButton, and GlassNavBar composables that produce backdrop blur,
chroma lift, and an edge sheen — with built-in quality tiers that degrade gracefully on
low-end Android and older iOS devices.
Compose's Modifier.blur blurs a composable's own content, not the backdrop behind it.
Chris Banes's haze library solves the backdrop-blur
problem beautifully, but it doesn't auto-degrade for memory-constrained devices — and the
iOS 26 "liquid glass" effect is heavy enough that Apple disables it on older hardware.
liquid-glass is an opinionated, iOS-26-flavored take on glassmorphism with three explicit
quality tiers and platform auto-detection:
| Tier | Blur radius | Saturation | Backdrop layer | Auto-picked on |
|---|---|---|---|---|
| Full | 24.dp | 1.4× | Full-res | Android 12+ (non-low-RAM), iOS 17+, Desktop, Web |
| Medium | 16.dp | 1.2× | 0.5× downsampled | iOS 15–16 (opt-in elsewhere) |
| Fallback | 0.dp | 1.0× | None — zero alloc | Android < 12 or isLowRamDevice, iOS < 15 |
Fallback allocates zero offscreen buffers and skips the blur entirely — so the same
code runs without OOMing on a 2 GB Android 11 device and still looks reasonable.
| Platform | Supported | Auto-tier |
|---|---|---|
| Android | ✅ | Full on API 31+ (non low-RAM), Fallback otherwise |
| iOS | ✅ | Full on iOS 17+, Medium on iOS 15–16, Fallback on < 15 |
| Desktop | ✅ | Full |
| Web | ✅ | Full |
gradle/libs.versions.toml:
[libraries]
liquid-glass = { module = "io.github.nadeemiqbal:liquid-glass", version = "0.2.3" }commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.liquid.glass)
}
}
}@Composable
fun Screen() {
val state = rememberLiquidGlassState() // auto-picks the tier for the device
Box(Modifier.fillMaxSize()) {
// 1) Anything inside this box becomes the backdrop that the glass samples from.
// Drop a `scenery.png` into
// `composeApp/src/commonMain/composeResources/drawable/`
// so `Res.drawable.scenery` is generated and shared across all targets.
Image(
painter = painterResource(Res.drawable.scenery),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize().liquidGlassSource(state),
)
// 2) A floating glass card on top, sampling the backdrop above.
GlassCard(
state = state,
modifier = Modifier.align(Alignment.Center).padding(24.dp),
) {
Text("Frosted, light-refracting surface, drop-in")
}
}
}Don't want to add an image? The bundled sample (
sample/composeApp/.../SampleApp.kt) uses a programmaticColorfulBackdropcomposable withliquidGlassSource(state)applied to it, and demonstrates every tier without any resource setup.
That's it. Same code on Android, iOS, Desktop, and Web — and the same code on a low-RAM Android 11 device will quietly fall back to a flat tint + edge sheen with no GraphicsLayer allocation.
Override individual parameters at the call site; the per-tier values from
LiquidGlassDefaults.forQuality are the sensible defaults:
GlassCard(
state = state,
shape = RoundedCornerShape(28.dp),
blurRadius = 30.dp,
saturation = 1.6f,
tint = Color.White.copy(alpha = 0.35f),
borderHighlight = Brush.verticalGradient(
0f to Color.White.copy(alpha = 0.6f),
1f to Color.Transparent,
),
grain = 0.04f, // subtle frosted texture (v0.2.0+)
refraction = 0.4f, // SkSL/AGSL pixel-offset distortion (v0.2.0+)
) { /* … */ }Derive the sheen color from the captured backdrop, instead of the static white gradient:
GlassCard(
state = state,
borderHighlight = rememberDynamicSheen(state),
) { /* … */ }The library polls the backdrop layer every 500 ms (configurable), averages its pixels, and produces a vertical-gradient brush from that color. Falls back to the static brush on platforms where backdrop sampling isn't available (e.g. wasmJs).
if (showDialog) {
GlassDialog(onDismissRequest = { showDialog = false }) {
Text("Material 3 Dialog with a liquid-glass surface.")
}
}
if (showSheet) {
GlassBottomSheet(onDismissRequest = { showSheet = false }) {
Text("Material 3 ModalBottomSheet with a liquid-glass surface.")
// …list items, etc.
}
}Both dialogs and bottom sheets host their own LiquidGlassState, so the glass samples the
dialog's own composition (typically just the system scrim), not the host activity behind it.
This matches how Modifier.blur and haze behave with dialogs.
Force a specific tier (e.g. for a brand-mandated "Full everywhere" experience):
val state = rememberLiquidGlassState(LiquidGlassQuality.Full)…or downgrade for low-end shells without waiting for auto-detection:
val state = rememberLiquidGlassState(LiquidGlassQuality.Fallback)| Symbol | Purpose |
|---|---|
LiquidGlassQuality |
Full / Medium / Fallback
|
rememberPlatformLiquidGlassQuality() |
Platform-detected tier (@Composable expect) |
rememberLiquidGlassState(quality) |
Holds the shared backdrop GraphicsLayer (or null for Fallback) |
Modifier.liquidGlassSource(state) |
Marks the backdrop composable |
Modifier.liquidGlass(state, …) |
Marks the glass surface — supports blur, saturation, tint, sheen, grain, refraction |
GlassCard(state, …, content) |
Modifier.liquidGlass wrapped around a padded Box
|
GlassButton(state, onClick, …) |
Pill-shaped clickable glass surface |
GlassNavBar(state, …, content) |
Modifier.liquidGlass over a status-bar-padded top bar |
GlassDialog(onDismissRequest, …, content) |
Material 3 Dialog with a glass surface |
GlassBottomSheet(onDismissRequest, …, content) |
Material 3 ModalBottomSheet with a glass surface |
rememberDynamicSheen(state, …) |
Edge-sheen Brush sampled from the captured backdrop's average color |
rememberGlassNoiseTile(seed, size) |
Procedural noise ImageBitmap for the grain parameter |
LiquidGlassDefaults |
Per-tier blurRadius, saturation, downsampleFactor, tints |
| Library | Backdrop blur | Per-device tiers | Zero-alloc fallback | API style |
|---|---|---|---|---|
Modifier.blur |
❌ | ❌ | — | Blurs own content only |
Material 3 Surface with tonalElevation
|
❌ | ❌ | — | Flat tint, no blur |
| haze | ✅ | ❌ | ❌ | Generic, very flexible |
| liquid-glass | ✅ | ✅ | ✅ | Opinionated iOS-26 look |
GlassDialog and GlassBottomSheet Material 3 wrappers — shipped in v0.2.0
RenderEffect (blur + saturation) on API 31+ — shipped in v0.2.0
Modifier.iosNativeGlass() interop using UIVisualEffectView / UIGlassEffect on iOS 26+See CONTRIBUTING.md.
iOS 26-style frosted glass surfaces for Compose Multiplatform. A Modifier.liquidGlass()
plus GlassCard, GlassButton, and GlassNavBar composables that produce backdrop blur,
chroma lift, and an edge sheen — with built-in quality tiers that degrade gracefully on
low-end Android and older iOS devices.
Compose's Modifier.blur blurs a composable's own content, not the backdrop behind it.
Chris Banes's haze library solves the backdrop-blur
problem beautifully, but it doesn't auto-degrade for memory-constrained devices — and the
iOS 26 "liquid glass" effect is heavy enough that Apple disables it on older hardware.
liquid-glass is an opinionated, iOS-26-flavored take on glassmorphism with three explicit
quality tiers and platform auto-detection:
| Tier | Blur radius | Saturation | Backdrop layer | Auto-picked on |
|---|---|---|---|---|
| Full | 24.dp | 1.4× | Full-res | Android 12+ (non-low-RAM), iOS 17+, Desktop, Web |
| Medium | 16.dp | 1.2× | 0.5× downsampled | iOS 15–16 (opt-in elsewhere) |
| Fallback | 0.dp | 1.0× | None — zero alloc | Android < 12 or isLowRamDevice, iOS < 15 |
Fallback allocates zero offscreen buffers and skips the blur entirely — so the same
code runs without OOMing on a 2 GB Android 11 device and still looks reasonable.
| Platform | Supported | Auto-tier |
|---|---|---|
| Android | ✅ | Full on API 31+ (non low-RAM), Fallback otherwise |
| iOS | ✅ | Full on iOS 17+, Medium on iOS 15–16, Fallback on < 15 |
| Desktop | ✅ | Full |
| Web | ✅ | Full |
gradle/libs.versions.toml:
[libraries]
liquid-glass = { module = "io.github.nadeemiqbal:liquid-glass", version = "0.2.3" }commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.liquid.glass)
}
}
}@Composable
fun Screen() {
val state = rememberLiquidGlassState() // auto-picks the tier for the device
Box(Modifier.fillMaxSize()) {
// 1) Anything inside this box becomes the backdrop that the glass samples from.
// Drop a `scenery.png` into
// `composeApp/src/commonMain/composeResources/drawable/`
// so `Res.drawable.scenery` is generated and shared across all targets.
Image(
painter = painterResource(Res.drawable.scenery),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize().liquidGlassSource(state),
)
// 2) A floating glass card on top, sampling the backdrop above.
GlassCard(
state = state,
modifier = Modifier.align(Alignment.Center).padding(24.dp),
) {
Text("Frosted, light-refracting surface, drop-in")
}
}
}Don't want to add an image? The bundled sample (
sample/composeApp/.../SampleApp.kt) uses a programmaticColorfulBackdropcomposable withliquidGlassSource(state)applied to it, and demonstrates every tier without any resource setup.
That's it. Same code on Android, iOS, Desktop, and Web — and the same code on a low-RAM Android 11 device will quietly fall back to a flat tint + edge sheen with no GraphicsLayer allocation.
Override individual parameters at the call site; the per-tier values from
LiquidGlassDefaults.forQuality are the sensible defaults:
GlassCard(
state = state,
shape = RoundedCornerShape(28.dp),
blurRadius = 30.dp,
saturation = 1.6f,
tint = Color.White.copy(alpha = 0.35f),
borderHighlight = Brush.verticalGradient(
0f to Color.White.copy(alpha = 0.6f),
1f to Color.Transparent,
),
grain = 0.04f, // subtle frosted texture (v0.2.0+)
refraction = 0.4f, // SkSL/AGSL pixel-offset distortion (v0.2.0+)
) { /* … */ }Derive the sheen color from the captured backdrop, instead of the static white gradient:
GlassCard(
state = state,
borderHighlight = rememberDynamicSheen(state),
) { /* … */ }The library polls the backdrop layer every 500 ms (configurable), averages its pixels, and produces a vertical-gradient brush from that color. Falls back to the static brush on platforms where backdrop sampling isn't available (e.g. wasmJs).
if (showDialog) {
GlassDialog(onDismissRequest = { showDialog = false }) {
Text("Material 3 Dialog with a liquid-glass surface.")
}
}
if (showSheet) {
GlassBottomSheet(onDismissRequest = { showSheet = false }) {
Text("Material 3 ModalBottomSheet with a liquid-glass surface.")
// …list items, etc.
}
}Both dialogs and bottom sheets host their own LiquidGlassState, so the glass samples the
dialog's own composition (typically just the system scrim), not the host activity behind it.
This matches how Modifier.blur and haze behave with dialogs.
Force a specific tier (e.g. for a brand-mandated "Full everywhere" experience):
val state = rememberLiquidGlassState(LiquidGlassQuality.Full)…or downgrade for low-end shells without waiting for auto-detection:
val state = rememberLiquidGlassState(LiquidGlassQuality.Fallback)| Symbol | Purpose |
|---|---|
LiquidGlassQuality |
Full / Medium / Fallback
|
rememberPlatformLiquidGlassQuality() |
Platform-detected tier (@Composable expect) |
rememberLiquidGlassState(quality) |
Holds the shared backdrop GraphicsLayer (or null for Fallback) |
Modifier.liquidGlassSource(state) |
Marks the backdrop composable |
Modifier.liquidGlass(state, …) |
Marks the glass surface — supports blur, saturation, tint, sheen, grain, refraction |
GlassCard(state, …, content) |
Modifier.liquidGlass wrapped around a padded Box
|
GlassButton(state, onClick, …) |
Pill-shaped clickable glass surface |
GlassNavBar(state, …, content) |
Modifier.liquidGlass over a status-bar-padded top bar |
GlassDialog(onDismissRequest, …, content) |
Material 3 Dialog with a glass surface |
GlassBottomSheet(onDismissRequest, …, content) |
Material 3 ModalBottomSheet with a glass surface |
rememberDynamicSheen(state, …) |
Edge-sheen Brush sampled from the captured backdrop's average color |
rememberGlassNoiseTile(seed, size) |
Procedural noise ImageBitmap for the grain parameter |
LiquidGlassDefaults |
Per-tier blurRadius, saturation, downsampleFactor, tints |
| Library | Backdrop blur | Per-device tiers | Zero-alloc fallback | API style |
|---|---|---|---|---|
Modifier.blur |
❌ | ❌ | — | Blurs own content only |
Material 3 Surface with tonalElevation
|
❌ | ❌ | — | Flat tint, no blur |
| haze | ✅ | ❌ | ❌ | Generic, very flexible |
| liquid-glass | ✅ | ✅ | ✅ | Opinionated iOS-26 look |
GlassDialog and GlassBottomSheet Material 3 wrappers — shipped in v0.2.0
RenderEffect (blur + saturation) on API 31+ — shipped in v0.2.0
Modifier.iosNativeGlass() interop using UIVisualEffectView / UIGlassEffect on iOS 26+See CONTRIBUTING.md.