diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
index 55daec06a8c6..14d21031a984 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java
@@ -132,6 +132,7 @@ public BaseViewManager(@Nullable ReactApplicationContext reactContext) {
view.setTag(R.id.use_hardware_layer, null);
view.setTag(R.id.filter, null);
+ view.setTag(R.id.backdrop_filter, null);
view.setTag(R.id.mix_blend_mode, null);
LayerEffectsHelper.apply(view, null, null);
@@ -242,6 +243,14 @@ public void setFilter(@NonNull T view, @Nullable ReadableArray filter) {
view.setTag(R.id.filter, filter);
}
+ @ReactProp(name = ViewProps.BACKDROP_FILTER, customType = "Filter")
+ public void setBackdropFilter(@NonNull T view, @Nullable ReadableArray backdropFilter) {
+ view.setTag(R.id.backdrop_filter, backdropFilter);
+ if (view.getParent() instanceof View) {
+ ((View) view.getParent()).invalidate();
+ }
+ }
+
@ReactProp(name = ViewProps.MIX_BLEND_MODE)
public void setMixBlendMode(@NonNull T view, @Nullable String mixBlendMode) {
view.setTag(R.id.mix_blend_mode, BlendModeHelper.parseMixBlendMode(mixBlendMode));
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt
index c1d63abe3a7e..9d7dbcfbb694 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManagerDelegate.kt
@@ -86,6 +86,8 @@ public abstract class BaseViewManagerDelegate<
ViewProps.FILTER -> mViewManager.setFilter(view, value as ReadableArray?)
+ ViewProps.BACKDROP_FILTER -> mViewManager.setBackdropFilter(view, value as ReadableArray?)
+
ViewProps.MIX_BLEND_MODE -> mViewManager.setMixBlendMode(view, value as String?)
ViewProps.SHADOW_COLOR ->
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt
index 7eb0f4378e6f..0c3cccf9ff22 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/FilterHelper.kt
@@ -117,15 +117,18 @@ internal object FilterHelper {
// https://www.w3.org/TR/filter-effects-1/#blurEquivalent
fun createBlurEffect(sigma: Float, chainedEffects: RenderEffect? = null): RenderEffect? {
- if (sigma <= 0.5) {
+ // RenderEffect.createBlurEffect takes sigma (standard deviation) directly — it passes
+ // through to Skia's SkImageFilters::Blur(sigmaX, sigmaY). Convert dp→px here; do NOT
+ // apply sigmaToRadius (that formula is for HWUI's box-filter BlurMaskFilter, not RenderEffect).
+ val pxSigma = PixelUtil.toPixelFromDIP(sigma)
+ if (pxSigma <= 0.5f) {
return null
}
- val radius: Float = sigmaToRadius(sigma)
return if (chainedEffects == null) {
- RenderEffect.createBlurEffect(radius, radius, Shader.TileMode.DECAL)
+ RenderEffect.createBlurEffect(pxSigma, pxSigma, Shader.TileMode.DECAL)
} else {
- RenderEffect.createBlurEffect(radius, radius, chainedEffects, Shader.TileMode.DECAL)
+ RenderEffect.createBlurEffect(pxSigma, pxSigma, chainedEffects, Shader.TileMode.DECAL)
}
}
@@ -199,7 +202,7 @@ internal object FilterHelper {
val color: Int = if (filterValues.hasKey("color")) filterValues.getInt("color") else Color.BLACK
val radius: Float =
if (filterValues.hasKey("standardDeviation"))
- sigmaToRadius(filterValues.getDouble("standardDeviation").toFloat())
+ PixelUtil.toPixelFromDIP(filterValues.getDouble("standardDeviation").toFloat())
else 0f
return createDropShadowEffect(offsetX, offsetY, radius, color, chainedEffects)
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt
index 1d0a305b2f2b..708773b090e2 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.kt
@@ -146,6 +146,7 @@ public object ViewProps {
public const val BORDER_START_COLOR: String = "borderStartColor"
public const val BORDER_END_COLOR: String = "borderEndColor"
public const val BOX_SHADOW: String = "boxShadow"
+ public const val BACKDROP_FILTER: String = "backdropFilter"
public const val FILTER: String = "filter"
public const val MIX_BLEND_MODE: String = "mixBlendMode"
public const val OUTLINE_COLOR: String = "outlineColor"
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/BackdropFilterHelper.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/BackdropFilterHelper.kt
new file mode 100644
index 000000000000..8029ff9305a7
--- /dev/null
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/BackdropFilterHelper.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.react.views.view
+
+import android.annotation.SuppressLint
+import android.annotation.TargetApi
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.RenderEffect
+import android.graphics.RenderNode
+import android.os.Build
+import com.facebook.react.bridge.ReadableArray
+import com.facebook.react.uimanager.FilterHelper
+
+/**
+ * Helper object for applying CSS backdrop-filter effects on Android.
+ *
+ * Applies filter effects to the composited backdrop (the pixels behind a view) captured as a
+ * [Bitmap] snapshot. Color-matrix filters (brightness, contrast, grayscale, hue-rotate, invert,
+ * opacity, saturate, sepia) work on all supported API levels. Blur and drop-shadow require API 31+
+ * via [RenderEffect] drawn into the hardware-accelerated canvas via [RenderNode].
+ */
+@SuppressLint("UseRequiresApi")
+internal object BackdropFilterHelper {
+
+ /**
+ * Draws a filtered region of [source] directly onto [canvas] at the child's position.
+ *
+ * @param canvas Hardware-accelerated canvas of the parent [ReactViewGroup].
+ * @param source Full-size backdrop snapshot bitmap (same dimensions as the parent view).
+ * @param destLeft Left coordinate of the child in parent space (canvas destination).
+ * @param destTop Top coordinate of the child in parent space (canvas destination).
+ * @param srcLeft Clamped left bound of the crop region within [source].
+ * @param srcTop Clamped top bound of the crop region within [source].
+ * @param srcRight Clamped right bound of the crop region within [source].
+ * @param srcBottom Clamped bottom bound of the crop region within [source].
+ * @param filters ReadableArray of filter descriptors as produced by processFilter.js.
+ */
+ fun drawFilteredBackdrop(
+ canvas: Canvas,
+ source: Bitmap,
+ destLeft: Int,
+ destTop: Int,
+ srcLeft: Int,
+ srcTop: Int,
+ srcRight: Int,
+ srcBottom: Int,
+ filters: ReadableArray,
+ ) {
+ val cropWidth = srcRight - srcLeft
+ val cropHeight = srcBottom - srcTop
+ if (cropWidth <= 0 || cropHeight <= 0) return
+
+ val cropped = Bitmap.createBitmap(source, srcLeft, srcTop, cropWidth, cropHeight)
+ try {
+ when {
+ FilterHelper.isOnlyColorMatrixFilters(filters) -> {
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ paint.colorFilter = FilterHelper.parseColorMatrixFilters(filters)
+ canvas.drawBitmap(cropped, destLeft.toFloat(), destTop.toFloat(), paint)
+ }
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
+ drawWithRenderEffect(canvas, cropped, destLeft.toFloat(), destTop.toFloat(), filters)
+ }
+ else -> {
+ // Pre-API 31: blur and drop-shadow are not supported; draw the backdrop unfiltered so
+ // at least the element's transparency is composited correctly.
+ canvas.drawBitmap(cropped, destLeft.toFloat(), destTop.toFloat(), null)
+ }
+ }
+ } finally {
+ cropped.recycle()
+ }
+ }
+
+ @TargetApi(Build.VERSION_CODES.S)
+ private fun drawWithRenderEffect(
+ canvas: Canvas,
+ source: Bitmap,
+ destLeft: Float,
+ destTop: Float,
+ filters: ReadableArray,
+ ) {
+ val renderEffect: RenderEffect =
+ FilterHelper.parseFilters(filters)
+ ?: run {
+ canvas.drawBitmap(source, destLeft, destTop, null)
+ return
+ }
+
+ val renderNode = RenderNode("backdropFilter")
+ renderNode.setPosition(0, 0, source.width, source.height)
+ renderNode.setRenderEffect(renderEffect)
+
+ val recordCanvas = renderNode.beginRecording()
+ recordCanvas.drawBitmap(source, 0f, 0f, null)
+ renderNode.endRecording()
+
+ canvas.save()
+ canvas.translate(destLeft, destTop)
+ canvas.drawRenderNode(renderNode)
+ canvas.restore()
+ }
+}
diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
index 363f1336d000..87e9578f9c1b 100644
--- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
+++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactViewGroup.kt
@@ -13,8 +13,10 @@ import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.Context
import android.content.res.Configuration
+import android.graphics.Bitmap
import android.graphics.BlendMode
import android.graphics.Canvas
+import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.drawable.Drawable
@@ -29,6 +31,7 @@ import com.facebook.react.R
import com.facebook.react.bridge.ReactNoCrashSoftException
import com.facebook.react.bridge.ReactSoftExceptionLogger
import com.facebook.react.bridge.ReactSoftExceptionLogger.logSoftException
+import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.UiThreadUtil.assertOnUiThread
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
@@ -988,11 +991,45 @@ public open class ReactViewGroup public constructor(context: Context?) :
}
}
+ // Snapshot of the parent's composited content (background + previous siblings) used to apply
+ // backdrop-filter effects on children that request them.
+ private var backdropBitmap: Bitmap? = null
+ private var backdropCanvas: Canvas? = null
+ private var hasBackdropFilterChild = false
+
+ override fun onDetachedFromWindow() {
+ super.onDetachedFromWindow()
+ backdropBitmap?.recycle()
+ backdropBitmap = null
+ backdropCanvas = null
+ }
+
override fun dispatchDraw(canvas: Canvas) {
if (_overflow != Overflow.VISIBLE || getTag(R.id.filter) != null) {
clipToPaddingBox(this, canvas)
}
+
+ hasBackdropFilterChild =
+ (0 until childCount).any { getChildAt(it).getTag(R.id.backdrop_filter) != null }
+
+ if (hasBackdropFilterChild && width > 0 && height > 0) {
+ val bmp =
+ backdropBitmap?.takeIf { it.width == width && it.height == height }
+ ?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888).also {
+ backdropBitmap?.recycle()
+ backdropBitmap = it
+ }
+ bmp.eraseColor(Color.WHITE)
+ val snapCanvas = Canvas(bmp)
+ backdropCanvas = snapCanvas
+ background?.draw(snapCanvas)
+ }
+
super.dispatchDraw(canvas)
+
+ if (hasBackdropFilterChild) {
+ backdropCanvas = null
+ }
}
override fun drawChild(canvas: Canvas, child: View, drawingTime: Long): Boolean {
@@ -1002,6 +1039,31 @@ public open class ReactViewGroup public constructor(context: Context?) :
enableZ(canvas, true)
}
+ val backdropFilter: ReadableArray? =
+ if (hasBackdropFilterChild) child.getTag(R.id.backdrop_filter) as? ReadableArray else null
+ val snapBitmap = backdropBitmap
+ if (backdropFilter != null && snapBitmap != null) {
+ val childLeft = child.left
+ val childTop = child.top
+ val srcLeft = childLeft.coerceAtLeast(0).coerceAtMost(snapBitmap.width)
+ val srcTop = childTop.coerceAtLeast(0).coerceAtMost(snapBitmap.height)
+ val srcRight = child.right.coerceAtLeast(0).coerceAtMost(snapBitmap.width)
+ val srcBottom = child.bottom.coerceAtLeast(0).coerceAtMost(snapBitmap.height)
+ if (srcRight > srcLeft && srcBottom > srcTop) {
+ BackdropFilterHelper.drawFilteredBackdrop(
+ canvas,
+ snapBitmap,
+ childLeft,
+ childTop,
+ srcLeft,
+ srcTop,
+ srcRight,
+ srcBottom,
+ backdropFilter,
+ )
+ }
+ }
+
var mixBlendMode: BlendMode? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && needsIsolatedLayer(this)) {
mixBlendMode = child.getTag(R.id.mix_blend_mode) as? BlendMode
@@ -1025,6 +1087,12 @@ public open class ReactViewGroup public constructor(context: Context?) :
canvas.restore()
}
+ // Update snapshot so subsequent backdrop-filter siblings see this child's pixels.
+ val snapCanvas = backdropCanvas
+ if (hasBackdropFilterChild && snapCanvas != null && backdropFilter == null) {
+ super.drawChild(snapCanvas, child, drawingTime)
+ }
+
if (drawWithZ) {
enableZ(canvas, false)
}
diff --git a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml
index d03b719892c1..0f865c9c32ea 100644
--- a/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml
+++ b/packages/react-native/ReactAndroid/src/main/res/views/uimanager/values/ids.xml
@@ -80,6 +80,9 @@
+
+
+
diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp
index 713ab1470fd0..0dab5a8c9e6d 100644
--- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp
+++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp
@@ -203,6 +203,12 @@ BaseViewProps::BaseViewProps(
{})),
filter(
convertRawProp(context, rawProps, "filter", sourceProps.filter, {})),
+ backdropFilter(convertRawProp(
+ context,
+ rawProps,
+ "backdropFilter",
+ sourceProps.backdropFilter,
+ {})),
backgroundImage(convertRawProp(
context,
rawProps,
@@ -390,6 +396,7 @@ void BaseViewProps::setProp(
RAW_SET_PROP_SWITCH_CASE_BASIC(outlineStyle);
RAW_SET_PROP_SWITCH_CASE_BASIC(outlineWidth);
RAW_SET_PROP_SWITCH_CASE_BASIC(filter);
+ RAW_SET_PROP_SWITCH_CASE_BASIC(backdropFilter);
RAW_SET_PROP_SWITCH_CASE_BASIC(boxShadow);
RAW_SET_PROP_SWITCH_CASE_BASIC(mixBlendMode);
// events field
diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h
index b72c5f944f63..fbde3d8d2524 100644
--- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h
+++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h
@@ -74,6 +74,9 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
// Filter
std::vector filter{};
+ // Backdrop Filter
+ std::vector backdropFilter{};
+
// Background Image
std::vector backgroundImage{};