Skip to content

Repository files navigation

react-native-android-liquid-glass

npm version npm downloads license platform architecture

Liquid Glass / glassmorphism for React Native on Android — real AGSL refraction (not a flat blur), chromatic dispersion, adaptive tint, and tilt-driven specular highlights. Built as a Fabric bridge over QWEA0/Liquid-Glass-Android, so you can bring an iOS 26 Liquid Glass look to Android View UIs.

Also searched as: frosted glass, glass blur, backdrop blur, UIGlassEffect for Android, expo-glass-effect Android alternative.

The effect is Android-only and New Architecture only. The package is safe to import and render everywhere. On iOS, web, macOS, Windows and in Jest the components degrade to a styled View — a dimming scrim, a correctly clamped corner radius and an edge hairline — rather than throwing. See Cross-platform behaviour. For native glass on iOS 26, pass your own renderFallback backed by UIGlassEffect / expo-glass-effect.

React Native Liquid Glass on Android — glassmorphism pill over a home screen icon grid

AGSL liquid glass lens playground with refraction controls Liquid glass shape merging like mercury metaballs Glassmorphism UI recipes — nav bar, tab bar, media card Liquid Glass basics — material and corner radius

Why this library

React Native has no built-in Liquid Glass. Most “glass” packages are either iOS-only (UIGlassEffect / expo-glass-effect) or a simple frosted blur. This package is for apps that need the Android side of that material:

You want… This package
iOS 26–style liquid glass on Android ✅ AGSL SDF lens (API 33+)
Real refraction / dispersion, not only blur ✅ lens + classic pipelines
Glass that works in lists, nav bars, tab bars ✅ recipes + ScrollEdgeBlurView
Safe import on iOS / web / Jest ✅ degraded View + renderFallback
Expo Go ❌ needs a dev build / prebuild (native module)
npm install react-native-android-liquid-glass
# or
yarn add react-native-android-liquid-glass

Features

  • Real AGSL lens on API 33+ — SDF refraction, per-channel dispersion, normal-lit specular highlight, with an automatic CPU/GPU blur fallback down to API 24.
  • 42 props across both pipelines, fully typed, with upstream's defaults spelled out so JS and native never drift.
  • Liquid shape merging — two glass bodies that smooth-min blend and cling to each other like mercury, driven through Fabric commands.
  • Adaptive tint that samples backdrop luminance and tells you when to flip your foreground colour.
  • Gravity-driven specular — the rim catches light as the device tilts.
  • ScrollEdgeBlurView — progressive edge blur for content scrolling under a translucent header.
  • Frame-time diagnostics through a natively throttled event, so you can tune on a real mid-range device instead of guessing.
  • No runtime dependencies beyond React Native itself.

Requirements

React Native 0.85, New Architecture (Fabric)
minSdk 24 — the AGSL lens pipeline needs API 33+, below that it falls back
compileSdk 36
ABIs arm64-v8a, armeabi-v7a only (upstream ships no x86 .so)
Upstream com.github.QWEA0:liquidglass:v2.0.0

Installation

npm install react-native-android-liquid-glass

The upstream library is published on JitPack, and android/build.gradle declares that repository itself. If your app sets RepositoriesMode.FAIL_ON_PROJECT_REPOS in android/settings.gradle, that declaration is ignored and you have to add it in the app instead:

dependencyResolutionManagement {
  repositories {
    google()
    mavenCentral()
    maven { url "https://jitpack.io" }
  }
}

To pin a different upstream version, set liquidGlassVersion in your app's android/build.gradle:

buildscript {
  ext {
    liquidGlassVersion = "v2.0.0"
  }
}

The coordinate in the upstream README (com.github.QWEA0.Liquid-Glass-Android:liquidglass) does not resolve — the module overrides its own groupId. The working one is com.github.QWEA0:liquidglass, which is what this package uses.

Quick start

import { Text, View } from 'react-native';
import { LiquidGlassAndroidView } from 'react-native-android-liquid-glass';

<View collapsable={false} style={{ flex: 1 }}>
  {/* whatever should show through the glass */}
  <YourContent />

  <LiquidGlassAndroidView
    style={{ width: 260, height: 140 }}
    cornerRadius={28}
    material="regular"
    refractionHeight={66}
    enableDynamicBackground
  >
    <Text>Liquid Glass</Text>
  </LiquidGlassAndroidView>
</View>;

The component is a FrameLayout natively, so children render on top of the glass — put your content inside it.

Three rules that decide whether this works

Almost every "the effect doesn't show up" case is one of these.

1. The backdrop must be in the glass's immediate parent

Upstream captures the backdrop with view.parent as? View. It draws that one parent's subtree, and nothing above it. So:

  • Content you want refracted has to be a sibling of the glass, under the same parent. A sibling of the parent is invisible to the capture.
  • Anything drawn after the glass in that same parent lands in the capture too. Put overlays and controls outside the parent, or they appear smeared inside the glass.
  • Add collapsable={false} to that parent. React Native flattens away views it considers redundant, which silently re-parents the glass and changes what it captures.

2. enableDynamicBackground={false} if nothing moves

This defaults to true, which is not upstream's field default. Nothing in React Native invalidates the glass when a sibling repaints, so with it off the backdrop is captured once and then frozen — scrolling content slides past while the glass keeps showing a stale image, which reads as a broken effect rather than a saving.

It is not free: upstream implements it by calling invalidate() at the end of every draw, so it is an unbounded redraw loop at display refresh rate plus a backdrop re-capture per frame, for as long as the view is mounted. Turn it off for genuinely static glass over static content, and reach for paused when a view stays mounted but stops being visible.

3. The backdrop needs high-frequency detail

Refraction bends the backdrop. A flat colour bent is still that flat colour, and a smooth gradient barely changes. Over solid backgrounds the effect is invisible and the library looks broken. The example app ships flat and gradient backdrops specifically so you can see this failure mode.

Props

Every dimension marked dp is converted to px natively. Props upstream declares as unitless floats — including displacementScale and dispersionThickness, which are map-space scalars rather than screen distances — are passed straight through.

Lens pipeline (API 33+)

Prop Type Default
material 'regular' | 'clear' 'regular'
cornerRadius number (dp) 999 (pill)
bevelWidth number (dp) 14
refractionHeight number (dp) 66
dispersionStrength number 01 0.1
enableSensorHighlight boolean false
enableAdaptiveTint boolean false
useShaderPipeline boolean true

refractionHeight is the dominant knob — at 0 the glass flattens into a plain blur. material="clear" blurs far less and applies a fixed dimming layer instead of adaptive tint; it is for glass over photos and video.

Blur

Prop Type Default
enableDynamicBackground boolean true
enableBackdropBlur boolean true
paused boolean false
layerType 'hardware' | 'none' 'hardware'
blurScale number (unitless multiplier) 0.0625
blurMethod 'smart' | 'boxBlur' | 'boxBlurCpp' | 'iirGaussian' | 'iirGaussianNeon' | 'downsample' | 'box3' 'smart'
highQualityBlur boolean false
downsampleScale int, 2–3 2
globalDownsampleFactor number 0.251 1
useHardwareBlurWhenPossible boolean true
enableOptimizedCapture boolean false

blurScale is a unitless multiplier: the effective radius is (overLight ? 12 : 4) + blurScale * 32 pixels. It is not @react-native-community/blur's 0–100 pixel radius — copying blurAmount={20} from that library gives a 644 px radius here, and __DEV__ warns about it. (blurAmount is the old name for this prop and still works for one minor version.)

downsampleScale is clamped by upstream to 2–3, so raising it further is not a tuning lever. Use globalDownsampleFactor instead.

paused stops the enableDynamicBackground redraw loop without unmounting — set it whenever the view is offscreen but still mounted, such as behind a pushed navigation screen or on an inactive tab.

Chromatic aberration

Prop Type Default
enableChromaticAberration boolean true
aberrationIntensity number 2
aberrationDownsample number 0.251 0.5
aberrationRedOffset number 0
aberrationGreenOffset number -0.05
aberrationBlueOffset number -0.1
aberrationUseBilinearInterpolation boolean true
chromaticAberrationMode 'auto' | 'cpp' | 'kotlin' 'auto'

Physical dispersion

Prop Type Default
enableChromaticDispersion boolean false
dispersionThickness number (unitless) 100
dispersionFactor number 1.5
dispersionGain number 7
dispersionDownsample number 0.251 0.5

The most expensive effect in the library. Measure it before shipping it, and never put it in a list.

Edges, shadow, tone

Prop Type Default
enableEdgeHighlight boolean true
edgeHighlightBorderWidth number (dp) 0.5
edgeHighlightOpacity number 0100 (percent) 100
enableShadow boolean false
saturation number (percent, 100 = unchanged) 140
overLight boolean false

edgeHighlightOpacity and saturation are percentages, not the 01 React Native uses for opacity. Upstream's units are kept rather than silently rescaled.

Displacement & touch

Prop Type Default
displacementScale number (unitless) 70
displacementMode 'standard' | 'polar' | 'prominent' 'standard'
elasticity number 0.15

Accessibility & diagnostics

Prop Type Default
accessibilityMode 'auto' | 'forceFull' | 'forceOpaque' 'auto'
collectFrameStats boolean true
frameStatsInterval int, ms (0 = off) 0

auto degrades to an opaque material when the system asks for reduced transparency or high-contrast text. forceFull overrides a user's explicit accessibility preference — reach for it only when your own layer already guarantees contrast.

Events

<LiquidGlassAndroidView
  enableAdaptiveTint
  onGlassAppearanceChange={(e) => setDark(!e.nativeEvent.isOverLight)}
  onPipelineReady={(e) => console.log(e.nativeEvent.deviceApiLevel)}
  frameStatsInterval={250}
  onFrameStats={(e) => setFps(e.nativeEvent.drawFps)}
/>
Event Payload Notes
onGlassAppearanceChange { isOverLight } Fires when backdrop luminance crosses the light/dark threshold, with hysteresis. Needs enableAdaptiveTint.
onPipelineReady { deviceApiLevel, supportsLensPipeline } One-shot, after the first prop commit.
onFrameStats { drawFps, totalMs, captureMs, blurMs, effectMs, finalizeMs, effectName, blurRecomputed, effectRecomputed, processedWidth, processedHeight } Requires collectFrameStats and frameStatsInterval > 0.

onFrameStats is throttled natively. Upstream invokes its listener on every drawn frame; forwarding that across the bridge at 60fps would cost more than the effect, so frameStatsInterval={0} (the default) never attaches a JS-visible listener at all. It is a development HUD — ship it off.

supportsLensPipeline is a device capability check, not a promise about the frame you are looking at: useShaderPipeline={false} or an accessibility degradation still force the classic path on a capable device.

Imperative API

Shapes are commands, not props. Grab a ref:

import { useEffect, useRef } from 'react';
import type { LiquidGlassAndroidViewHandle } from 'react-native-android-liquid-glass';

const glass = useRef<LiquidGlassAndroidViewHandle>(null);

useEffect(() => {
  // Two glass bodies that smooth-min blend — close the gap and they merge.
  glass.current?.setPrimaryShape({
    x: 20, y: 40, width: 96, height: 96, cornerRadius: 48,
  });
  glass.current?.setSecondaryShape({
    x: 150, y: 40, width: 96, height: 96, cornerRadius: 48, smoothing: 16,
  });
}, []);

<LiquidGlassAndroidView ref={glass} style={{ width: 300, height: 190 }} />;
Method Notes
setPrimaryShape(shape) Glass geometry inside the view, dp, relative to its top-left. Lens pipeline only.
clearPrimaryShape() Back to filling the whole view.
setSecondaryShape(shape) A second body, smooth-min blended with the first.
clearSecondaryShape() Remove it.
refreshAccessibilityState() Re-read system settings, e.g. from an AppState active handler after the user returns from Settings.

Omitting cornerRadius reproduces upstream's defaults: the view's current cornerRadius for the primary shape, a pill for the secondary.

ScrollEdgeBlurView

The library's second component: a progressive blur pinned to one edge, so content dissolves as it scrolls under a header. It is a plain View, takes no children, and should be positioned absolutely.

import { ScrollEdgeBlurView } from 'react-native-android-liquid-glass';

<ScrollEdgeBlurView
  pointerEvents="none"
  style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 90 }}
  edge="top"
  maxBlurRadius={40}
/>;
Prop Type Default
edge 'top' | 'bottom' 'top'
maxBlurRadius number, raw px 40

maxBlurRadius is the one prop that is not converted from dp: upstream clamps it to 0–100 pixels, so converting would cut the usable range to about 33dp on a 3x screen. Scale it with PixelRatio.get() yourself if you want it density-independent.

Its handle exposes bindScrollView(ref) / unbindScrollView(). In practice ViewTreeObserver scroll callbacks are window-wide so it usually redraws either way, but binding documents the intent.

Cross-platform behaviour

Importing and rendering this package is safe on every platform. On anything other than Android both components render a degraded View:

Android prop Fallback mapping
cornerRadius borderRadius, clamped to half the shorter side, borderCurve: 'continuous'
material, overLight a dimming backgroundColor scrim
enableEdgeHighlight, edgeHighlightBorderWidth, edgeHighlightOpacity borderWidth + borderColor
enableShadow shadow*
everything blur/refraction/dispersion related dropped

The scrim is not decoration. On Android the regular material dims the backdrop, which is what makes white-on-glass text readable — a fully transparent fallback would put white text on a white background with no crash and no warning. The clamp is not polish either: an unclamped borderRadius: 999 renders as a square on iOS whenever a border is present.

onPipelineReady fires once on the fallback with supportsLensPipeline: false, so a capability gate resolves instead of hanging. The imperative handle is complete, with isSupported: false and commands that return false rather than throwing.

Branching without rendering

import { getGlassCapabilities, useGlassSupport } from 'react-native-android-liquid-glass';

const { supported, supportsLensPipeline } = useGlassSupport();

Supplying your own fallback

<LiquidGlassAndroidView
  style={styles.panel}
  renderFallback={({ style, children }) => (
    <GlassView style={style}>{children}</GlassView>   // e.g. expo-glass-effect
  )}
>
  <Text>Now playing</Text>
</LiquidGlassAndroidView>

This keeps expo-glass-effect out of this package's dependency graph entirely.

Error handling

Both components take an onError handler. It fires when a prop value is rejected, an enum string is unrecognised, a command throws natively, or a capability had to be demoted for the device:

<LiquidGlassAndroidView
  onError={({ nativeEvent: { code, prop, message, fatal } }) => {
    if (fatal) setUseGlass(false);
    else console.warn(`[glass] ${code} on ${prop}: ${message}`);
  }}
/>

Codes: NATIVE_BLUR_UNAVAILABLE, PIPELINE_DEGRADED, INVALID_ENUM, INVALID_VALUE, COMMAND_FAILED, BIND_FAILED, EVENT_DISPATCH_UNAVAILABLE. Most are fatal: false — the bridge recovered — but they always mean the view is not doing what the props asked for. Everything is logged to logcat under the LiquidGlassAndroidView tag even with no handler.

In __DEV__ a JS validation layer additionally warns (once per prop) about the things upstream would silently swallow: non-finite numbers, the percentage props that look like 0–1 ranges, values outside upstream's silent clamps, and prop combinations that can never fire.

Presets

import { LiquidGlassAndroidView, GlassPresets } from 'react-native-android-liquid-glass';

<LiquidGlassAndroidView preset="floatingTabBar" cornerRadius={32} />

preset takes navigationBar, floatingTabBar, cardOverMedia or compactControl; dispersionPreset takes subtle, glass, crystal, diamond or rainbow. Both are resolved in JS as { ...preset, ...yourProps }, so an explicit prop always wins. The raw maps are exported as GlassPresets and DISPERSION_PRESETS.

Performance

The effect captures and re-blurs the backdrop. Rough order of what to reach for:

  1. paused={true} whenever the view is mounted but not visible. This is the biggest single win: enableDynamicBackground drives a self-invalidating redraw at display refresh rate that nothing else stops, including behind a pushed navigation screen.
  2. globalDownsampleFactor to 0.5 — quarters the pixels the capture and blur touch, and the blur hides most of the resolution loss.
  3. enableDynamicBackground={false} wherever nothing actually moves.
  4. enableChromaticDispersion={false} — by far the heaviest effect.
  5. aberrationDownsample / dispersionDownsample down. (downsampleScale is clamped to 2–3 upstream, so it is not the lever it looks like.)
  6. At most one enableAdaptiveTint view per screen. It costs a periodic software View.draw() of the glass's parent subtree on the UI thread every 350 ms, on every pipeline. Drive the rest from its onGlassAppearanceChange.
  7. layerType="none" if you have large or numerous glass views — upstream forces a hardware layer, which is an off-screen texture per instance. Measure it.

Pass StyleSheet.created styles and useCallback-wrapped handlers: both components are wrapped in memo, and inline styles or arrow handlers defeat the bailout, forcing a full native prop re-commit on every ancestor render.

Do not stack many instances in a scrolling list. Keep the frame-stats demo open on a real mid-range device while tuning, and read maxTotalMs rather than totalMs — it covers every frame in the window, so spikes are visible.

Known limitations

  • Touches mostly stop at the glass. pointerEvents="none" / "box-none" now stops the glass claiming touches at the React Native level, which is what you want for a decorative panel. It cannot be complete: upstream v2.0.0's onTouchEvent returns true for every down/move/up with no opt-out, and Android dispatches to a disabled view's onTouchEvent anyway. elasticity={0} does not disable the capture. Upstream main's enablePressEffect is the real fix and will be adopted when released.
  • The native blur kernels are arm only. The AAR ships libnativegauss.so for arm64-v8a and armeabi-v7a only, and the failure surfaces as ExceptionInInitializerError / NoClassDefFoundErrorjava.lang.Errors that upstream's own catch (Exception) cannot stop. The bridge probes for the library at view creation and demotes blurMethod to boxBlur, chromaticAberrationMode to kotlin, and forces enableChromaticDispersion off, reporting NATIVE_BLUR_UNAVAILABLE through onError. So an x86_64 emulator renders a softer effect instead of crashing — but use an arm64 emulator (the default on Apple Silicon) or a device to see the real thing.
  • Props for the inactive pipeline are ignored. Nothing throws on API 30 — you just get a blur where a lens was designed. __DEV__ warns for the cases where this is most surprising (primaryShape, enableChromaticDispersion).
  • Upstream main is ahead of v2.0.0. enablePressEffect, pressScale, debugApiLevelCap and effectiveApiLevel exist on main but not in the released artifact, so they are deliberately not bridged: the module would not compile against the version people can actually install.
  • setCustomBackdropCapture is not exposed. It takes a Kotlin lambda returning a Bitmap and has no sensible JS equivalent.

Example app

17 demos covering the whole bridged surface — a home-screen showcase, a playground per prop group, the shape commands, all three events, and four copy-paste UI recipes. Screenshots live in docs/media/.

yarn                # install
yarn example start  # metro
yarn android        # build + run

Run on an arm64 emulator or a physical device.

Development

yarn typecheck      # tsc
yarn lint           # eslint
yarn codegen        # regenerate the Fabric interface/delegate to read them

Adding a prop is two steps, and the compiler enforces the second:

  1. Declare it in src/LiquidGlassAndroidViewNativeComponent.ts.
  2. Implement the generated override fun setXxx(...) in android/src/main/java/com/liquidglassandroid/LiquidGlassAndroidViewManager.kt.

See docs/BRIDGING.md for the full mechanics.

Contributing

License

MIT. The upstream Liquid-Glass-Android library is also MIT.


Made with create-react-native-library

About

iOS 26–style Liquid Glass / glassmorphism for React Native on Android — AGSL refraction, dispersion, adaptive tint. Fabric bridge over QWEA0/Liquid-Glass-Android.

Topics

Resources

Code of conduct

Contributing

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages