rn-blur-overlay
v2.0.1
Published
GPU-accelerated Gaussian blur for React Native — content blur and true backdrop blur (glassmorphism), with Skia-equivalent sigma and no Skia dependency. iOS uses real-time QuartzCore layer filters over UIVisualEffectView; Android 12+ uses RenderEffect on
Maintainers
Readme
<BlurView blur={20}>{content}</BlurView> // blur the children
<BlurView mode="backdrop" blur={20} /> // blur what's behind
<BlurView mode="backdrop" blur={30} fadeDirection="down" /> // ramp it offWhy this exists
Most React Native blur libraries hand you an opaque 0–100 "intensity" dial. A Figma spec of
backdrop-blur: 10.85px becomes a number someone tunes by eye — to a different value on each
platform. Several also snapshot their backdrop once per frame on the main thread: fine for one
static chip, ruinous on a scrolling list.
rn-blur-overlay takes a different position on both:
bluris a Gaussian sigma in DP — the same unit asreact-native-skia's<Blur blur={σ} />and CSSbackdrop-filter: blur(). A Figma value transfers verbatim, and iOS and Android agree.- The default paths never snapshot. iOS drives QuartzCore layer filters; Android 12+ drives a
RenderEffecton aRenderNode. Both recompute on the platform's own render thread against a live display list, so content scrolling underneath updates the blur for free.
Install
npm install rn-blur-overlay
cd ios && pod installThat is the whole setup. Autolinked on both platforms — no MainApplication edit, no manual
ReactPackage registration, no Gradle changes.
New Architecture note. The native side is legacy view managers, which Fabric renders through its interop layer. That layer is on by default and needs no configuration — but it is explicitly transitional in React Native's own source. Fabric-native components are on the roadmap.
Quick start
Content blur — blur the children
Blurs this view's own subtree. Any children: images, text, nested containers, icons, gradients, or any mix of them. Nothing branches on child type.
import { BlurView } from 'rn-blur-overlay';
import { Image, StyleSheet, View } from 'react-native';
export const BlurredCover = ({ uri }: { uri: string }) => (
<View style={styles.frame}>
<BlurView blur={20}>
<Image source={{ uri }} style={StyleSheet.absoluteFill} resizeMode="cover" />
</BlurView>
</View>
);
const styles = StyleSheet.create({
frame: { width: 300, height: 200, overflow: 'hidden' },
});Content mode fills its parent by default — see rule 3 if you want it to size to its children instead.
Backdrop blur — frosted glass
Wrap what should be blurred in <BlurTarget>, then place the <BlurView> as its sibling.
import { BlurTarget, BlurView } from 'rn-blur-overlay';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
export const GlassBar = () => (
<View style={styles.root}>
<BlurTarget style={StyleSheet.absoluteFill}>
<ScrollView>{/* … */}</ScrollView>
</BlurTarget>
<BlurView mode="backdrop" blur={24} tint="#FFFFFF33" saturation={1.8} style={styles.bar}>
<Text style={styles.label}>Stays sharp on top of the blur</Text>
</BlurView>
</View>
);
const styles = StyleSheet.create({
root: { flex: 1 },
bar: {
position: 'absolute', left: 16, right: 16, bottom: 32,
height: 88, borderRadius: 24, justifyContent: 'center', paddingHorizontal: 20,
},
label: { color: '#FFFFFF', fontWeight: '600' },
});Children of a backdrop BlurView draw on top of the blur, unblurred. That is how you get crisp
labels on a glass panel.
Progressive blur — ramp it off
Fully blurred at one edge, sharp at the other. Works in both modes.
<BlurView
mode="backdrop"
blur={30}
fadeDirection="down" // blurred at the top, sharp at the bottom
fadeStart={0.3} // hold full blur through the first 30%
fadeEnd={1} // fully sharp by the bottom edge
style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 140 }}
/>The sigma scale
blur is σ, not a percentage. Rules of thumb for picking one:
| σ | Reads as | Typical use |
|:--:|---|---|
| 2–4 | a faint frost | de-emphasised text, disabled surfaces, subtle depth |
| 6–12 | clear glass | nav bars, tab bars, floating toolbars, chips |
| 16–24 | heavy glass | modal scrims, sheets, cards over photography |
| 30+ | fully obscured | privacy screens, app-switcher masking, splash transitions |
Two boundaries worth knowing:
- Below σ 0.5 there is no blur on Android 12+.
RenderEffect's radius 0 already yields σ 0.5, so smaller values round away. UseblurEnabled={false}to mean "off". - Above σ 10.6 Android 7–11 downsamples. One RenderScript pass caps at radius 25 (σ ≈ 10.6), so larger sigmas are reached by blurring a smaller bitmap — softer and slightly less precise there.
Engines
The engine is picked automatically from mode × platform × fidelity × whether a fade or tint is
set. You never choose one — this is what each costs.
| Mode | Platform / condition | Engine | σ | Per-frame cost |
|---|---|---|:--:|---|
| backdrop | iOS 13.4+ · fidelity="exact" (default) | CAFilter gaussianBlur on the UIVisualEffectView backdrop layer | exact | none — render server recomputes |
| backdrop | iOS 13.4+ · fidelity="public" | UIVisualEffectView + paused UIViewPropertyAnimator | approx. | none |
| backdrop | Android 12+ · with BlurTarget | RenderEffect over the target's live display list | exact | none — one GPU pass, no readback |
| backdrop | Android 12+ · no BlurTarget | same, plus a re-record of the window root | exact | one extra tree traversal |
| backdrop | Android 7–11 | RenderScript over a downscaled snapshot | close | one capture when content or position changes |
| content | iOS 13.4+ · exact, no fade | CAFilter gaussianBlur on the view's own layer | exact | none — render server recomputes |
| content | iOS 13.4+ · public, or any fade | CIGaussianBlur on a Metal CIContext | exact | one capture per layout or child change |
| content | Android 12+ · no fade, no tint | RenderEffect on the view itself | exact | none — hwui blurs the layer |
| content | Android 12+ · fade or tint · and Android 7–11 | children re-recorded into a RenderNode, or a RenderScript bitmap below API 31 | exact / close | one re-record (or one bitmap pass) per draw |
Two paths snapshot instead of sampling live. It is worth knowing which:
- Android 7–11 draws the target into a downscaled bitmap and blurs it with RenderScript. It re-captures solely when the target's content or the blur view's position actually changed.
- iOS content blur with a fade, or with
fidelity="public", snapshots the subtree through Core Image. Capture happens on the main thread; the blur itself runs on a background queue. It refreshes on layout changes and child insertions — so animating or video children can go stale. Drop the fade (or keepfidelity="exact") and iOS blurs live instead.
Three rules that matter
Everything else is optional. These three are not.
1. A BlurView must be a sibling of its BlurTarget, never a descendant
// ✅ sibling — samples the target's subtree, and nothing else
<View>
<BlurTarget style={StyleSheet.absoluteFill}>{photo}</BlurTarget>
<BlurView mode="backdrop" blur={20} style={panel}>{label}</BlurView>
</View>
// ⚠️ nested — still renders, but the target has to be re-recorded every frame
<BlurTarget>
{photo}
<BlurView mode="backdrop" blur={20} />
</BlurTarget>This is what scopes the sample. Get it wrong and it still looks correct at first, which is what
makes it worth stating: a nested BlurView falls back to re-recording its enclosing BlurTarget,
and with no BlurTarget anywhere in the tree it re-records the window root. Both are a second
traversal per frame, and on Android sampling the window root means a panel can blur its own children
into a halo behind them. Android logs the reason once per view to logcat under the RNBlurView tag.
BlurTarget cannot capture SurfaceView-backed content — video, maps, GL. TextureView works on
Android 12+.
2. A backdrop blur paints over its container's backgroundColor
The blur draws the sampled backdrop across the whole view, so a fill underneath it is hidden. Put the
fill on tint, and keep the container colour only as a fallback for when the blur cannot draw (a
software canvas — e.g. a programmatic screenshot — falls back to tint alone).
// ❌ the blur covers this fill — the panel reads transparent
<View style={{ backgroundColor: '#FFFFFFCC' }}>
<BlurView mode="backdrop" blur={12} style={StyleSheet.absoluteFill} />
</View>
// ✅ tint carries the fill; the container keeps it as a fallback
<View style={{ backgroundColor: '#FFFFFFCC' }}>
<BlurView mode="backdrop" blur={12} tint="#FFFFFFCC" style={StyleSheet.absoluteFill} />
</View>3. Content mode fills its parent
mode="content" has to cover the children it blurs, so it defaults to position: absolute on all
four edges. That is what makes <BlurView blur={20}><Image style={StyleSheet.absoluteFill} /></BlurView>
work without sizing anything by hand.
It is a default, not a constraint — your style wins. To use it as an ordinary in-flow container
that hugs its children:
<BlurView blur={10} style={{ position: 'relative', padding: 16 }}>
<Text>Sized by its own content, blurred in place</Text>
</BlurView>mode="backdrop" gets no positioning default at all — it is a panel with its own place in the
layout.
API
<BlurView>
Accepts every standard View prop, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
| blur | number \| { x, y } | — | Required. Gaussian sigma (σ) in DP/points. Same unit as Skia and CSS. 0 disables. Pass { x, y } for an anisotropic blur. |
| mode | 'content' \| 'backdrop' | 'content' | Blur the children, or blur what is behind. |
| tint | ColorValue | — | Colour composited over the blur. Use a translucent one — see rule 2. |
| saturation | number | 1 | 0 = greyscale, 1.5–2 = Apple-material punch. |
| tileMode | 'clamp' \| 'repeat' \| 'mirror' \| 'decal' | 'clamp' | How the kernel samples past the edges. Android only — Core Image and CAFilter always clamp. |
| fadeDirection | 'up' \| 'down' \| 'left' \| 'right' | — | Enables progressive blur, fading out in this direction. |
| fadeStart | number | 0 | Where the ramp starts, 0–1. Fully blurred before it. |
| fadeEnd | number | 1 | Where the ramp ends, 0–1. Fully sharp after it. |
| blurEnabled | boolean | true | Switch the blur off without unmounting or rebuilding the native pipeline. |
| autoUpdate | boolean | true | Keep tracking the content behind. Only read on the Android backdrop paths that re-record — always on 7–11, and on 12+ when there is no BlurTarget. Ignored on the BlurTarget fast path and on iOS. |
| downsampleFactor | number | auto | Snapshot downscale, used as a floor. Android 7–11 only. |
| fidelity | 'exact' \| 'public' | 'exact' | See iOS fidelity. iOS only. |
| material | BlurMaterial | — | Apple material tint — thin, chrome, dark, ultraThin, … iOS mode="backdrop" only. |
| cornerRadii | [tl, tr, br, bl] | from style | Overrides the radii read from style. |
borderRadius (and the per-corner variants) are read from style and applied to the blur layer
itself, so <BlurView style={{ borderRadius: 24 }} /> rounds the blur — not just the border.
<BlurTarget>
Plain View props. Marks the subtree that a sibling BlurView samples.
On Android it records its children into a RenderNode as part of its normal draw, so the blur costs
one GPU pass and no extra UI-thread work — and it also lets the blur view know when the content
actually changed, which is what keeps the pre-Android-12 path from re-capturing needlessly. On iOS it
is a plain view: UIVisualEffectView already samples everything composited beneath it. Keeping it in
both trees means one JSX tree behaves identically on both platforms.
setDefaultBlurFidelity(fidelity)
import { setDefaultBlurFidelity } from 'rn-blur-overlay';
setDefaultBlurFidelity('public'); // opt the whole app out of the private-API pathCall once at startup, before anything renders. Views already mounted keep the value they were created with.
Types
BlurViewProps, BlurTargetProps, BlurMode, BlurTileMode, BlurFadeDirection, BlurFidelity,
and BlurMaterial are all exported.
Recipes
<View style={{ flex: 1 }}>
<BlurTarget style={StyleSheet.absoluteFill}>
<FlatList data={items} renderItem={renderItem} contentContainerStyle={{ paddingBottom: 96 }} />
</BlurTarget>
<BlurView
mode="backdrop"
blur={18}
tint="rgba(255,255,255,0.18)"
saturation={1.8}
style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 88, paddingBottom: 24 }}
>
<Tabs />
</BlurView>
</View><BlurView
mode="backdrop"
blur={28}
fadeDirection="down"
fadeStart={0.45}
style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 120 }}
pointerEvents="none"
/>Full blur behind the status bar, dissolving into sharp content by the bottom edge.
<BlurTarget style={StyleSheet.absoluteFill}>
<Screen />
</BlurTarget>
<BlurView
mode="backdrop"
blur={24}
tint="rgba(0,0,0,0.35)"
saturation={0.85}
style={StyleSheet.absoluteFill}
>
<Sheet />
</BlurView><View style={{ height: 240, overflow: 'hidden' }}>
<BlurView blur={16}>
<Image source={{ uri }} style={StyleSheet.absoluteFill} resizeMode="cover" />
</BlurView>
{/* sibling, not a child — so it stays sharp */}
<Text style={styles.title}>Now Playing</Text>
</View>In content mode the children are the thing being blurred, so anything that must stay crisp goes
outside the BlurView.
const [sigma, setSigma] = useState(0);
// …drive with Animated/Reanimated on the JS side, or just setState
<BlurView mode="backdrop" blur={sigma} tint="#FFFFFF22" style={panel} />Cheap on iOS exact and Android 12+ (a property change on an existing filter). Expensive on Android
7–11, where every value change means a fresh capture and RenderScript pass — animate a tint's opacity
there instead.
<BlurView mode="backdrop" blur={20} autoUpdate={false} style={panel} />Captures once, then holds. Useful for a static backdrop behind a modal on older Android, where staying live costs a capture whenever the content moves.
iOS fidelity
iOS has no public API for "blur the backdrop by exactly σ" — UIBlurEffect offers a handful of fixed
materials with no radius knob. So there are two paths.
'exact' (default) drives QuartzCore's real-time layer filters, the same mechanism
UIVisualEffectView uses internally. blur becomes a true Gaussian sigma matching Android and Skia,
and the render server recomputes it — nothing is snapshotted. This reaches CAFilter, which is not
public API. The mitigations: symbol names are base64-encoded so they never appear as literals in the
binary, resolved once at runtime, every lookup nil-checked, and any failure degrades to the public
path on its own. The same technique backs Flutter's BackdropFilter on iOS.
'public' uses public API only: a UIVisualEffectView material whose intensity is scrubbed with
a paused UIViewPropertyAnimator — the approach expo-blur takes. blur is then approximate rather
than an exact sigma, mapped onto 0–1 over roughly σ 0–40. Content blur under 'public' goes through
Core Image, which is an exact sigma, but snapshotted.
Switch per view with fidelity, or app-wide with
setDefaultBlurFidelity. Android is unaffected either way — every
Android path is public framework API.
Performance
- On iOS
exactand Android 12+, the blur is recomputed by the platform's render server or RenderThread. No bitmaps per frame, nothing on the JS thread. - Android 7–11 backdrop blur is the path to measure. If you support these versions and need blur
on a scrolling surface, check
dumpsys gfxinfobefore and after — or fall back to a translucent tint there. - Always add a
BlurTargetfor backdrop blur. Without one, every frame costs a second traversal of the window root. - Don't stack many backdrop blurs. On iOS, more than ~5 simultaneous
UIVisualEffectViews measurably hurts compositing. - Prefer
blurEnabled={false}over unmounting when toggling — it keeps the native pipeline built. - Android logs
ViewManagerPropertyUpdater: Could not find generated setter …at startup. Benign: props fall back to reflection because the package ships no annotation-processor output yet. See Roadmap.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Panel reads transparent, no glass | The blur painted over the container's backgroundColor | Move the fill to tint — rule 2 |
| Blur looks right but scrolling is janky on old Android | Android 7–11 snapshot path | autoUpdate={false}, raise downsampleFactor, or use a tint below API 31 |
| Video / map / GL content isn't blurred | SurfaceView composites outside the view's display list | Use a TextureView mode (Android 12+) if the library offers one |
| Panel's own children show as a halo behind them | No BlurTarget, so the window root — including the panel — is being sampled | Add a BlurTarget sibling — rule 1 |
| Nothing blurs at very low blur values | σ ≤ 0.5 rounds to no blur on Android 12+ | Use σ ≥ 1, and blurEnabled={false} for "off" |
| iOS content blur is stale over animating children | Fade or fidelity="public" put it on the Core Image snapshot path | Drop fadeDirection, or keep fidelity="exact" |
| borderRadius clips the view but not the blur | Radii come from style; a computed or overridden shape may not resolve | Pass cornerRadii={[tl, tr, br, bl]} explicitly |
| Blur missing in a programmatic screenshot | Software canvas can't draw a RenderNode | Expected — it degrades to tint alone; keep a container backgroundColor as backup |
| iOS blur looks like a fixed Apple material, not your σ | CAFilter lookup failed, so it degraded to the public path | Verify fidelity isn't set to 'public'; the fallback is by design |
Upgrading from 1.x
blurnow means the same sigma on both platforms. 1.x applied a hand-tunedσ × 2.2radius on Android with no density conversion, so Android over-blurred relative to iOS — by a different amount per screen density. Expect Android to be somewhat softer at the same value now, and consistent with iOS. Re-tune anything you calibrated on Android.- Existing usage keeps working.
modedefaults tocontent, and content mode still fills its parent. BlurTargetandmode="backdrop"are new. Nothing existing needs to change to adopt them.- Children of a
BlurVieware now laid out by React Native. Earlier versions let the underlyingFrameLayoutre-position them, stacking every child at the top-left.
Roadmap
- Fabric-native components. Today's view managers reach the New Architecture through RN's interop layer, which upstream marks as transitional.
- Generated prop setters, to drop the reflection fallback and its startup log.
- Android 7–11: evaluate retiring the RenderScript path (deprecated since API 31) in favour of a tint fallback.
Credits
The Android backdrop architecture follows the approach proven by
Dimezis/BlurView. Sigma parity is based on hwui's
Blur::convertRadiusToSigma (σ = 0.57735 × radius + 0.5) and RenderScript's
σ = 0.4 × radius + 0.6. The iOS backdrop-layer filter technique is the same one used by
aheze/VariableBlurView,
dominicstop/VisualEffectBlurView, and
Flutter's iOS engine.
License
MIT © Parthil Savaliya
