apple-liquid-glass-guide

Apple Liquid Glass Implementation Guide

This guide is self-contained. It has one goal only: to let anyone, by reading only this repository, rebuild the Apple iOS 26 “liquid glass” effect on native Android, and get it right — not the cheap counterfeit of “translucency + Gaussian blur”, but real glass with refraction, chromatic aberration, edge highlights, and gravity response.

Every conclusion comes from a real, end-to-end development process that ran through to a compiled APK (Jetpack Compose + Backdrop 2.0.0). Every parameter, every line of code, and every pitfall in this document has been verified in practice, not guessed.

Last updated: 2026-10-03 (field notes added: docs/10-Compose-Production-Field-Notes.md)


★ The Authoritative Upstream — READ THIS BEFORE ANYTHING ELSE

This project is built entirely from the upstream **Kyant0/AndroidLiquidGlass** (the Compose Multiplatform “Backdrop” liquid-glass engine, published as io.github.kyant0:backdrop:2.0.0). Everything in this repository — every effect, every API, every material recipe — is explained and implemented strictly according to that link. There is no other source of truth.

If you are picking up this repository cold, do this before touching anything:

  1. Read the upstream first: https://github.com/Kyant0/AndroidLiquidGlass — its README.md and Documentation (the Backdrop catalog) are the ground truth for how liquid glass actually works. A complete verbatim copy of that upstream is archived here at upstream/AndroidLiquidGlass/ (see UPSTREAM-README.md), so this repository is fully self-contained — you never need to leave it.
  2. Then read this repository’s docs/ — they are a practice-hardened, Android-native port of that same upstream, with exact recipes and every pitfall we hit. They do not replace the upstream; they are the bridge from upstream theory to a compiled APK. For how liquid glass actually works at the shader level, read docs/09-How-Liquid-Glass-Is-Implemented.md first.
  3. Never invent parameters. All core visual parameters in docs/04-Material-Recipe-Table.md are copied verbatim from the upstream. If you change any, record why.

In short: this repo says what the upstream means in practice. Read the link above first, then this repo.

The full upstream & license notes live at the end of this file in Section 6.


0. Read This First: Build the Right Mental Model in 30 Seconds

If you remember only one thing, remember this:

Liquid glass is not a “material property”; it is a real-time geometric refraction of the already-rendered image behind the glass.

This sentence determines the entire architecture:

Wrong understanding Correct understanding
Add alpha = 0.3f + Modifier.blur() to a Box First you need an offscreen texture of “the image behind”, then sample and warp it
The glass component draws itself The glass component only samples someone else’s texture
Modifier.blur() blurs its own content What gets blurred is the background; the glass’s own content stays sharp (this is the key to the glass feel)

So the whole tech stack has just three steps:

① Record the background into an offscreen texture   →   rememberLayerBackdrop() + Modifier.layerBackdrop()
② The glass component "samples" that texture        →   Modifier.drawBackdrop(backdrop = ...)
③ Warp it by the optical formula when sampling       →   effects = { vibrancy(); blur(..); lens(..) }

Without step ①, step ② draws nothing. This is the root cause of 90% of failures.


1. Ten Iron Rules (Read Before Coding — All Learned the Hard Way)

  1. No backdrop, no glass. The first argument of drawBackdrop is a Backdrop object. You must first create one with rememberLayerBackdrop(), then attach it to the “background node” with Modifier.layerBackdrop(it) so it has content to sample.

  2. The render pipeline order is fixed: colorFilter → blur → lens. So when writing the effects block, vibrancy() / colorControls() always go on the first line, blur() on the second, and lens() last. Putting lens() first = refraction happens on the pre-blur image, and the edges get smeared.

  3. Both arguments of lens() are dp, not pixels. You must use 12f.dp.toPx(). The first argument is the refraction height (how far the lens “bulges” — determines the refraction’s area of effect), the second is the refraction amount (how strong the offset is). Increasing both = thicker glass, more like a water drop.

  4. Chromatic aberration (chromaticAberration) is only on during “motion”. Static glass with aberration on → colored red/blue fringes at the edges → looks obviously fake. The upstream approach: when dragging/pressing, progress → 1, and aberration opens up with progress.

  5. Highlight is not a stroke; it is an “edge highlight band that fades by angle along the rim”. For that Apple “bright line along the edge” look, use Highlight, not your own border(1.dp, Color.White) — the latter is a hard, uniform edge and looks like a plastic frame.

  6. onDrawSurface is the glass’s “tint”, not a background color. Exercise extreme restraint. Control Center 5% black, Dock 22%–34% white, notification card 14% white. Once it exceeds 50%, the glass becomes a solid rounded rectangle and all refraction is covered up.

  7. Layering: a content layer can only refract its “layer below”; an upper overlay that wants to refract “the layer below the below” must merge backdrops. Use rememberCombinedBackdrop(a, b). This is the only correct way to make “opening the Control Center blurs the home-screen icons”.

  8. Never create a “refract-itself” loop. A node must not both layerBackdrop(itself) and drawBackdrop(itself), nor attach two recording nodes to the same LayerBackdrop (the second overwrites the first). The upstream approach: use an alpha(0f) shadow node to record, while a separate node does the actual glass drawing.

  9. The recording layer = one full-screen offscreen render per frame; it is the only performance cost of this approach. So do conditional recording: only attach layerBackdrop when an upper overlay actually needs to read it. Leaving it attached on a static page just wastes GPU for nothing.

  10. Use RoundedRectangle from com.kyant.shapes for clip() and for blur/vibrancy glass, NOT for drawBackdrop when lens() is present. Kyant shapes give Apple’s G2-continuous curvature (superellipse/squircle); RoundedCornerShape is plain rounded corners. All Apple icon corners are G2-continuous; pick the wrong shape for a non-refracting outline and it gives itself away as fake. CRITICAL (verified on device, see docs/10 §1): lens() builds its refraction SDF only from a Compose CornerBasedShape (RoundedCornerShape / CutCornerShape / RoundedRectangularShape). Handing it a com.kyant.shapes.RoundedRectangle throws UnsupportedOperationException: Only RoundedRectangularShape or CornerBasedShape is supported in lens effects at attach time → the app crashes on open. So whenever lens() is in the effects block, pass RoundedCornerShape(...) to drawBackdrop’s shape.


2. Repository Map

apple-liquid-glass-guide/         ← you are here
├── README.md                     ← this document (the overview; read this first)
├── UPSTREAM-README.md            ← the authoritative-upstream archive guide (see below)
├── upstream/
│   └── AndroidLiquidGlass/       ← ★ complete verbatim copy of Kyant0/AndroidLiquidGlass (the real engine source)
├── docs/
│   ├── 01-Optical-Model-of-Liquid-Glass.md      ← Principle: what refraction / dispersion / blur / highlight / shadow actually compute
│   ├── 02-Backdrop-Layered-Architecture.md      ← Architecture: LayerBackdrop / Combined / coordinate space / feedback prevention
│   ├── 03-API-Complete-Reference.md             ← API: all drawBackdrop parameters + effects + Highlight/Shadow explained one by one
│   ├── 04-Material-Recipe-Table.md              ← Recipes: exact parameters for 12 verified materials + how to derive new ones
│   ├── 05-iOS-Shell-in-Practice.md               ← In practice: how to assemble home / lock / control center / transitions
│   ├── 06-Icons-and-Asset-Pipeline.md            ← Assets: how to use ui-icons-hub (with the real "Apple app-icon zoning" list)
│   ├── 07-Build-and-Troubleshooting.md           ← Engineering: environment, build commands, all errors and fixes
│   ├── 08-Previous-Handover-Notes.md             ← History: handover notes from the previous round (26-component showcase), kept for reference
│   ├── 09-Practitioner-Notes.md                   ← Field notes: what 01–08 got wrong once shipped, + how to drop real glass onto a full iOS-style shell
│   ├── 09-How-Liquid-Glass-Is-Implemented.md     ← ★ READ THIS: explains, from the upstream source, exactly how liquid glass is implemented (recorder→sampler→AGSL refraction shader→highlight)
│   └── 10-Compose-Production-Field-Notes.md        ← Device-shipped field notes: the lens crash, real Face ID, asset sourcing, unlock guard, camera launch, crash banner
├── assets/
│   └── app-icons/                                 ← 40 real Apple-style app icons (PNG) pulled from ui-icons-hub, + manifest + fetch script
├── tools/
│   └── fetch_app_icons.py                         ← re-download the icon manifest and any icon, reproducibly
└── LiquidGlassShowcase/          ← complete compilable project (Jetpack Compose)

⚠️ This repository does not contain an APK. The APK is a build artifact, not source. To verify the effect, build one yourself following 07-Build-and-Troubleshooting.md: write sdk.dir in local.properties → point JAVA_HOME at JDK 17 → ./gradlew assembleRelease. If you can build a 1.25 MB app-release.apk, the whole liquid-glass environment has been successfully reproduced.

The 5 source files most worth reading (in order of importance):

File Why it matters
core/glass/GlassMaterials.kt The single source of truth for all materials: exact parameters for 12 recipes, each annotated with its upstream origin
core/glass/GlassScaffold.kt Minimal working example: how to record the wallpaper into a LayerBackdrop, in 18 lines
components/GlassFoundation.kt How to wrap drawBackdrop into a reusable GlassCard
components/GlassBars.kt The most complex example: combined backdrop + shadow recording node + dynamic dispersion; understand this and you understand it all
ios/IosShell.kt Layered architecture in practice: the z-order and backdrop assignment of the whole Apple system shell

3. Up and Running in Five Minutes: Minimal Runnable Example

This is all the code needed to get “liquid glass” running. Copy it in and you’ll see the effect.

3.1 Dependencies

// gradle/libs.versions.toml
[versions]
backdrop = "2.0.0"       // io.github.kyant0:backdrop —— liquid-glass engine
kyantShapes = "1.2.0"    // io.github.kyant0:shapes  —— Apple G2-continuous-curvature shapes (a transitive dependency of backdrop)

[libraries]
backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backdrop" }
kyant-shapes = { group = "io.github.kyant0", name = "shapes", version.ref = "kyantShapes" }
// app/build.gradle.kts
android {
    minSdk = 31          // hard requirement: AGSL needs API 31+
    compileSdk = 37
}
dependencies {
    implementation(libs.backdrop)
    implementation(libs.kyant.shapes)
}

⚠️ minSdk must be ≥ 31. This effect is built on AGSL (Android Graphics Shading Language), a runtime shader; below API 31 there is no RuntimeShader, which fails with ClassNotFound.

3.2 Step One: Record the Background into a Backdrop

@Composable
fun WallpaperScaffold(content: @Composable BoxScope.(backdrop: Backdrop) -> Unit) {
    Box(Modifier.fillMaxSize()) {
        // ① Create an offscreen texture
        val backdrop = rememberLayerBackdrop()

        Image(
            painter = painterResource(R.drawable.wallpaper),
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .fillMaxSize()
                .layerBackdrop(backdrop)   // ② Record this node's per-frame image into the texture
        )

        content(backdrop)                  // ③ Hand the texture to all upper glass components
    }
}

These two lines are the key to the whole approach. Whatever the node modified by layerBackdrop draws ends up in the backdrop.

3.3 Step Two: Draw a Piece of Glass

@Composable
fun GlassCard(backdrop: Backdrop, modifier: Modifier = Modifier) {
    Box(
        modifier.drawBackdrop(
            backdrop = backdrop,                       // which texture to sample
            // ⚠ NOTE (verified, docs/10 §1): with lens() present the shape MUST be a Compose
            // CornerBasedShape. RoundedRectangle(32.dp) from com.kyant.shapes crashes here.
            shape = { RoundedCornerShape(32.dp) },
            effects = {
                vibrancy()                             // ① boost saturation so the glass looks "luminous" not "grey"
                lens(16f.dp.toPx(), 32f.dp.toPx())     // ② refraction: the whole secret of glass thickness
            }
        )
    )
}

3.4 Step Three: How to Tune It to Look “Like Apple”

Only three knobs are at work; remember their directions:

Desired effect What to change Direction
Glass looks “thicker”, more like a water drop Increase both lens() arguments together 16→24, 32→48
Background blurrier / more “foggy” Increase blur() 2→8→16
Glass more “solid”, more obscuring Deepen onDrawSurface = { drawRect(color) } white 0.05 → 0.34
The bright edge line stronger highlight = { Highlight.Default.copy(alpha = 1f) } 0.5→1.0

Only these three directions. Everything else is detail. In Apple’s official demo, the Control Center uses “large lens + strong highlight”, the Dock uses “small lens + medium tint”, and dialogs use “large lens + large blur + depthEffect”. For concrete values see docs/04-Material-Recipe-Table.md.


4. One-Line Answers to Common Questions

Question Answer  
Why is my glass grey? Missing vibrancy(). Blur greys out colors; vibrancy() restores the saturation  
Why can’t I see anything through my glass? onDrawSurface color is too dark and covers the refraction. Drop it to 5%–35%  
Why are there red/blue fringes at the edges? chromaticAberration = true is always on. It should only open on press/drag  
Why are my glass edges hard-edged? You used border(). You should use highlight = { Highlight.Default }  
Why do the rounded corners look wrong? You used RoundedCornerShape for a non-refracting outline Use com.kyant.shapes.RoundedRectangle for clip()/non-lens glass. But if lens() is in effects, you must use RoundedCornerShape — a kyant shape crashes the app (docs/10 §1).
Why is my glass empty/black inside? The backdrop recorded nothing. Check whether layerBackdrop is attached to the node that actually draws the background  
Why does the glass show the previous frame when I open a panel? The recording node and the drawing node are ordered backwards. The content layer must draw before the overlay  
Why is it laggy? layerBackdrop is attached unconditionally. Change it to “record only when needed”  
Why does it crash on older phones? minSdk < 31, no AGSL  
Can I change the parameters myself? Yes, but change them proportionally; see the “Deriving new recipes” section in docs/04-Material-Recipe-Table.md  

5. Suggested Reading Order


6. Upstream and License