Skip to content
All posts

10 min read

React Native blur background: which BlurView to use

Three ways to blur in React Native and why they are not interchangeable: expo-blur, community blur, and Skia for when it has to animate.

For a blur background in React Native, use expo-blur's BlurView if your project has Expo modules, @react-native-community/blur if you are on bare React Native without them, and React Native Skia's BackdropFilter when the blur has to follow a gesture. They are not interchangeable, and the reason is one sentence: the two BlurView packages sample the real native view hierarchy behind them, while Skia only blurs pixels drawn inside its own Canvas.

Blur bugs in React Native usually come down to one of four things: Android was never enabled, the blur is inside a Modal, the thing underneath is a video, or somebody animated intensity at 60fps. Here is each path, the code that works, and where it stops working.

Which blur library should I use in React Native?

Option What it can blur Android Reach for it when
expo-blur Real views behind it Opt-in, needs a blur target The default. Headers, tab bars, sheet backdrops.
@react-native-community/blur Real views behind it Built in, older engine Bare RN with no Expo modules installed.
Skia BackdropFilter Only pixels in the same Canvas Identical to iOS Blur radius is driven by a gesture or a spring.
expo-glass-effect Real views behind it Falls back to a plain View You specifically want iOS 26 Liquid Glass.

One practical note on the community package before you pick it: @react-native-community/blur is still at 4.4.1, published August 2024. It works, it is not abandoned in spirit, but it is not moving. If you are on Expo, or you can install Expo modules into a bare app, expo-blur is the one getting the Android work. There is also a newer independent package, @sbaiahmed1/react-native-blur, which ships progressive blur and a Liquid Glass view and is New Architecture only. It is worth a look if you need a gradient blur, which none of the others give you out of the box.

How do I blur a background with expo-blur?

On iOS this is close to free. BlurView maps onto UIVisualEffectView, so the blur happens in the system compositor, not in your JavaScript and not in your render pass. Scroll a list under it and nothing in your app does extra work.

import { BlurView } from 'expo-blur';
import { FlatList, StyleSheet, Text } from 'react-native';

type Message = { id: string; subject: string };

export function InboxHeader({ messages }: { messages: Message[] }) {
  return (
    <>
      <FlatList
        data={messages}
        keyExtractor={(m) => m.id}
        renderItem={({ item }) => <Text style={styles.row}>{item.subject}</Text>}
      />

      {/* rendered AFTER the list, so it composites on top */}
      <BlurView intensity={70} tint="systemChromeMaterial" style={styles.header}>
        <Text style={styles.title}>Inbox</Text>
      </BlurView>
    </>
  );
}

const styles = StyleSheet.create({
  header: {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    paddingTop: 56,
    paddingBottom: 12,
    paddingHorizontal: 16,
    borderRadius: 24,
    overflow: 'hidden', // without this, borderRadius is ignored
  },
  title: { fontSize: 17, fontWeight: '600' },
  row: { padding: 16 },
});

Two details that trip people up. Order matters: render the BlurView after the scrolling content, not before it. And borderRadius alone does not clip the blur, you need overflow: 'hidden' on the same style. A rounded blurred tab bar with square corners is almost always that missing line. If you want to see the pattern already assembled, our mail tab bar drop is a blurred bar over a scrolling list.

intensity is 1 to 100 and defaults to 50. tint defaults to 'default' and accepts the whole iOS material vocabulary, from 'systemUltraThinMaterial' through 'systemChromeMaterialDark'. Those names describe iOS materials; Android approximates them with a tint colour, so do not expect a pixel match across platforms and do not design as if you will get one.

Why doesn't the blur work on Android?

Because you have not turned it on. By default expo-blur on Android renders a semi-transparent view and no blur at all. That is the single most common "expo-blur is broken" report, and it is the documented default.

The prop name depends on your SDK. Through Expo SDK 54 it is experimentalBlurMethod, with values 'none' (the default) and 'dimezisBlurView'. In newer SDKs it was renamed to blurMethod, gained a third value 'dimezisBlurViewSdk31Plus', and, importantly, Android now needs you to say what to blur by wrapping that content in a BlurTargetView and handing its ref to the blur.

// Expo SDK 55+
import { BlurView, BlurTargetView } from 'expo-blur';
import { useRef } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';

type Message = { id: string; subject: string };

export function Screen({ messages }: { messages: Message[] }) {
  const target = useRef<View | null>(null);

  return (
    <View style={{ flex: 1 }}>
      {/* everything you want blurred has to live inside the target */}
      <BlurTargetView ref={target} style={StyleSheet.absoluteFill}>
        <FlatList
          data={messages}
          keyExtractor={(m) => m.id}
          renderItem={({ item }) => <Text style={styles.row}>{item.subject}</Text>}
        />
      </BlurTargetView>

      <BlurView
        blurTarget={target}
        blurMethod="dimezisBlurViewSdk31Plus"
        intensity={70}
        style={styles.header}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  header: { position: 'absolute', top: 0, left: 0, right: 0, height: 96 },
  row: { padding: 16 },
});

// Expo SDK 54 and earlier: no BlurTargetView, and the prop is
// experimentalBlurMethod="dimezisBlurView"

Now the API-level part, which explains why Android blur has a reputation. A cheap blur needs the RenderEffect API, added in Android SDK 31 (Android 12). Below 31 the library falls back to RenderScript, which is deprecated and slow. 'dimezisBlurView' runs everywhere and will cost you frames on old devices. 'dimezisBlurViewSdk31Plus' uses the fast path on Android 12 and up and degrades to no blur below it. Unless you have a hard design requirement on Android 11, pick the second one and let old phones get a translucent panel.

blurReductionFactor (Android, default 4) is not a performance dial, whatever the name suggests. Android divides the blur radius by it, so it exists to bring the Android result closer to what the same intensity gives you on iOS. Raise it and the blur gets weaker and sharper; drop it to 2 and the same intensity blurs about twice as hard. Leave it alone until you have both platforms side by side and one of them is visibly off.

Blur inside a Modal will not work on Android. A React Native Modal is a separate Android window, so a blur inside it has no access to the pixels of the window behind it, and a BlurTargetView ref cannot cross that boundary. Render your overlay as an absolutely positioned sibling inside the same screen instead of reaching for Modal. If your blur works on iOS and shows a grey box on Android, check for a Modal before you check anything else.

Can you blur a video in React Native?

On iOS, yes, and it needs no special handling. UIVisualEffectView blurs whatever is composited beneath it, and video is composited like everything else, so a blurred control bar over a playing video just works.

On Android, no, not with a view-hierarchy blur. Video renders into its own SurfaceView on a separate surface that the blur pipeline cannot read, so you get the tint and none of the blur. Your options are to switch the player to a TextureView output where the player supports it, to blur a paused poster frame instead of the live video, or to draw a gradient scrim and stop pretending.

Read the small print on that first option before you build on it. The Dimezis library underneath expo-blur can only blur a TextureView on API 31 and above, and nothing that is SurfaceView-based at all. So the workaround costs you extra compositing work and does nothing on Android 11 and below, which means you still need the scrim as the floor. Design the scrim first and treat the blur as the upgrade.

How do you animate a blur without dropping frames?

Animate opacity, not intensity. Opacity is a compositing operation the GPU does for free. Intensity is a new blur pass: capture the backdrop, downsample it, blur it, upload it, and on Android that whole pipeline reruns on every frame you change the value. Visually the difference between fading a blur in and ramping its radius is small. In frame time it is not.

Here is a bottom sheet backdrop that fades a blur in over 240ms, closes on tap, and keeps every frame on the UI thread with Reanimated v4.

import { useEffect, type RefObject } from 'react';
import { Pressable, StyleSheet, View } from 'react-native';
import { BlurView } from 'expo-blur';
import Animated, {
  Easing,
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

type Props = {
  open: boolean;
  onClose: () => void;
  // ref to the BlurTargetView wrapping the screen; without it Android
  // renders a flat tint, no matter what blurMethod says
  blurTarget: RefObject<View | null>;
};

export function SheetBackdrop({ open, onClose, blurTarget }: Props) {
  const progress = useSharedValue(0);

  useEffect(() => {
    progress.value = withTiming(open ? 1 : 0, {
      duration: 240,
      easing: Easing.out(Easing.cubic),
    });
  }, [open, progress]);

  const fade = useAnimatedStyle(() => ({ opacity: progress.value }));

  return (
    <AnimatedPressable
      accessibilityLabel="Close sheet"
      pointerEvents={open ? 'auto' : 'none'}
      onPress={onClose}
      style={[StyleSheet.absoluteFill, fade]}
    >
      <BlurView
        blurTarget={blurTarget}
        blurMethod="dimezisBlurViewSdk31Plus"
        intensity={64}
        tint="dark"
        style={StyleSheet.absoluteFill}
      />
    </AnimatedPressable>
  );
}

The pointerEvents line matters more than it looks. A fully transparent full-screen view still eats every touch, so a closed backdrop you forgot to disable will silently break the screen underneath it. Note the blurTarget coming in as a prop, too: the backdrop cannot own that ref, because the thing being blurred is the screen that renders the backdrop. Wrap the screen in a BlurTargetView once and thread the ref down, or Android quietly gives you a flat dark rectangle.

If you genuinely want the blur to build up rather than fade in, wrap the blur itself and drive the prop:

import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
import Animated, {
  interpolate,
  useAnimatedProps,
  type SharedValue,
} from 'react-native-reanimated';

const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);

// progress comes from the parent (the same shared value the sheet animates),
// so the hook still runs inside a component
export function RampedBlur({ progress }: { progress: SharedValue<number> }) {
  const blurProps = useAnimatedProps(() => ({
    intensity: interpolate(progress.value, [0, 1], [0, 64]),
  }));

  return (
    <AnimatedBlurView
      animatedProps={blurProps}
      tint="dark"
      style={StyleSheet.absoluteFill}
    />
  );
}

Test that one on a mid-range Android device before you ship it, not on a simulator. And test both versions with Reduce Transparency turned on in iOS accessibility settings, because the system replaces the blur with a flat fill and a design that only reads at 64 intensity will read as nothing.

When should you reach for Skia instead?

When the blur radius is a number you are animating continuously, especially from a gesture. Skia's BackdropFilter accepts Reanimated shared values directly, so a drag can drive the radius on the UI thread with no prop round trip, and it behaves the same on both platforms.

import {
  BackdropFilter,
  Blur,
  Canvas,
  Image,
  Fill,
  rect,
  useImage,
} from '@shopify/react-native-skia';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';

type Props = { source: number; width: number; height: number };

export function FrostedPanel({ source, width, height }: Props) {
  // useImage resolves the asset into the SkImage that <Image> needs
  const photo = useImage(source);
  const blur = useSharedValue(0);

  // drag up to frost the panel, drag down to clear it, all on the UI thread
  const pan = Gesture.Pan().onChange((e) => {
    'worklet';
    blur.value = Math.min(24, Math.max(0, blur.value - e.changeY / 12));
  });

  if (!photo) return null;

  return (
    <GestureDetector gesture={pan}>
      <Canvas style={{ width, height }}>
        <Image image={photo} fit="cover" x={0} y={0} width={width} height={height} />

        <BackdropFilter
          filter={<Blur blur={blur} />}
          clip={rect(0, height - 220, width, 220)}
        >
          <Fill color="rgba(0,0,0,0.15)" />
        </BackdropFilter>
      </Canvas>
    </GestureDetector>
  );
}

The catch is the one from the first paragraph, and it is worth repeating because it is the thing people discover after an hour of work: BackdropFilter blurs the pixels already drawn in that Canvas. It cannot blur a FlatList that lives outside the Canvas. If the content underneath is static, you can snapshot it with Skia's makeImageFromView, which takes a ref to a native view and resolves to an SkImage you draw into the Canvas. That is a great fit for a sheet, where the background is frozen anyway, and a bad fit for anything still moving.

What about Liquid Glass?

expo-glass-effect arrived in SDK 54 with GlassView and GlassContainer, built on UIVisualEffectView. It is iOS 26 and above only and falls back to a plain View elsewhere, so gate it with the exported isLiquidGlassAvailable() and keep a real BlurView path for everyone else. The package also exports isGlassEffectAPIAvailable(), which exists because some iOS 26 builds ship without the glass API and calling into it crashes, so check that one too before you touch the container APIs. Note that the availability check reports whether the effect exists, not whether the user has asked the system to stop using it, so still check AccessibilityInfo.isReduceTransparencyEnabled() before you rely on translucency to carry contrast.

The sheet the blur usually sits behind has its own free reference: action sheet.

Does expo-blur work on Android?

Yes, but not by default. Android renders a semi-transparent view unless you opt in through the blur method prop: experimentalBlurMethod on Expo SDK 54 and earlier, or blurMethod on newer SDKs, where you also have to wrap the content you want blurred in a BlurTargetView and pass its ref to the BlurView via blurTarget. Prefer the 'dimezisBlurViewSdk31Plus' method, which uses the fast RenderEffect API on Android 12 and above and falls back to no blur on older versions rather than to slow RenderScript.

Why is my BlurView not blurring inside a Modal on Android?

A React Native Modal is a separate Android window, so a blur rendered inside it cannot read the pixels of the window behind it, and a blur target ref cannot cross the window boundary. The fix is to stop using Modal for that overlay and render it as an absolutely positioned sibling inside the same screen, or to use a sheet library that renders in the same window. On iOS the same code works either way, which is why this only shows up in Android QA.

Can you blur a video background in React Native?

On iOS yes, with no extra work, because the system compositor blurs whatever is beneath the blur view including video layers. On Android, no: video renders into its own SurfaceView on a separate surface that the blur cannot sample, so you get the tint and no blur. Workarounds are switching the player to a TextureView output if it supports one, which only helps on Android 12 and above because the underlying Dimezis library can blur a TextureView on API 31+ and nothing SurfaceView-based at all, blurring a static poster frame instead of live playback, or replacing the blur with a gradient scrim on Android. Since the TextureView route leaves Android 11 and below with nothing, build the scrim first and treat the blur as the upgrade.

Is animating blur intensity expensive?

Yes, considerably more expensive than fading the blur in with opacity. Every change to intensity forces a fresh blur pass: capture the backdrop, downsample it, blur it and upload it, and on Android that whole pipeline repeats on every animated frame. Animate the opacity of a constant-intensity BlurView instead, which the GPU composites for free, and reserve animated intensity for short one-off transitions you have profiled on a real mid-range Android device.

What is the difference between expo-blur and @react-native-community/blur?

Both wrap the same native primitives: UIVisualEffectView on iOS and the Dimezis BlurView library on Android. The practical differences are maintenance and packaging. expo-blur ships with the Expo SDK, is versioned with it, and is where the recent Android work has landed; @react-native-community/blur has been at 4.4.1 since August 2024 and is the sensible choice mainly for bare React Native apps with no Expo modules installed. The prop names differ too: intensity and tint in expo-blur, blurAmount and blurType in the community package.

The short version

Reach for expo-blur first, turn Android on explicitly, and never animate intensity when a fade will read the same. Reach for Skia only when a gesture owns the blur radius and you are already drawing the content in a Canvas. And test every blur on a real mid-range Android phone with Reduce Transparency on, because a blur is a design decision that two platforms and one accessibility toggle are each allowed to overrule.

React NativeExpoReanimatedSkiaexpo-blurAndroid