From 54c679c6475b873fd3ee799047862fb3833c561d Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Thu, 5 Nov 2026 00:29:51 +0200 Subject: [PATCH 1/5] feat: add ignoreSystemBarsVisibility prop to SafeAreaProvider --- src/SafeArea.types.ts | 1 + src/SafeAreaContext.tsx | 25 ++++++++- src/__tests__/SafeAreaContext-test.tsx | 56 +++++++++++++++++++ .../SafeAreaContext-test.tsx.snap | 9 +++ .../__snapshots__/SafeAreaView-test.tsx.snap | 4 ++ src/specs/NativeSafeAreaProvider.ts | 2 + 6 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/SafeArea.types.ts b/src/SafeArea.types.ts index 462de645..f3f9fbb9 100644 --- a/src/SafeArea.types.ts +++ b/src/SafeArea.types.ts @@ -52,6 +52,7 @@ export interface NativeSafeAreaProviderProps extends ViewProps { ref?: React.Ref; children?: React.ReactNode; onInsetsChange: InsetChangeNativeCallback; + ignoreSystemBarsVisibility?: boolean; } export interface NativeSafeAreaViewProps extends ViewProps { diff --git a/src/SafeAreaContext.tsx b/src/SafeAreaContext.tsx index 8b1abaa2..345a3dbc 100644 --- a/src/SafeAreaContext.tsx +++ b/src/SafeAreaContext.tsx @@ -36,10 +36,22 @@ if (isDev) { 'SafeAreaCornerAdaptedInsetsContext'; } +const SafeAreaIgnoreSystemBarsVisibilityContext = React.createContext(false); +if (isDev) { + SafeAreaIgnoreSystemBarsVisibilityContext.displayName = + 'SafeAreaIgnoreSystemBarsVisibilityContext'; +} + export interface SafeAreaProviderProps extends ViewProps { ref?: React.Ref; children?: React.ReactNode; initialMetrics?: Metrics | null; + /** + * Report the insets as if the system bars were visible, so hiding or + * showing the status bar or the Android navigation bar does not change + * them. Defaults to the value of the parent provider, or `false`. + */ + ignoreSystemBarsVisibility?: boolean; /** * @deprecated */ @@ -50,10 +62,16 @@ export function SafeAreaProvider({ children, initialMetrics, initialSafeAreaInsets, + ignoreSystemBarsVisibility: ignoreSystemBarsVisibilityProp, style, ref, ...others }: SafeAreaProviderProps) { + const parentIgnoreSystemBarsVisibility = React.useContext( + SafeAreaIgnoreSystemBarsVisibilityContext, + ); + const ignoreSystemBarsVisibility = + ignoreSystemBarsVisibilityProp ?? parentIgnoreSystemBarsVisibility; const parentInsets = useParentSafeAreaInsets(); const parentFrame = useParentSafeAreaFrame(); const parentCornerInsets = React.useContext( @@ -148,6 +166,7 @@ export function SafeAreaProvider({ ref={ref} style={[styles.fill, style]} onInsetsChange={onInsetsChange} + ignoreSystemBarsVisibility={ignoreSystemBarsVisibility} {...others} > {insets != null ? ( @@ -156,7 +175,11 @@ export function SafeAreaProvider({ - {children} + + {children} + diff --git a/src/__tests__/SafeAreaContext-test.tsx b/src/__tests__/SafeAreaContext-test.tsx index ce952515..1cbe8b45 100644 --- a/src/__tests__/SafeAreaContext-test.tsx +++ b/src/__tests__/SafeAreaContext-test.tsx @@ -290,6 +290,62 @@ describe('SafeAreaContext', () => { ); }); }); + + describe('ignoreSystemBarsVisibility', () => { + function readIgnoreSystemBarsVisibility(testID: string) { + return screen.getByTestId(testID).props.ignoreSystemBarsVisibility; + } + + it('is off by default', () => { + render(); + expect(readIgnoreSystemBarsVisibility('provider')).toBe(false); + }); + + it('passes the prop to the native provider', () => { + render(); + expect(readIgnoreSystemBarsVisibility('provider')).toBe(true); + }); + + it('inherits the value of the parent provider', () => { + render( + + + , + ); + expect(readIgnoreSystemBarsVisibility('inner')).toBe(true); + }); + + it('lets a nested provider override the parent value', () => { + render( + + + + + , + ); + expect(readIgnoreSystemBarsVisibility('inner')).toBe(false); + expect(readIgnoreSystemBarsVisibility('innermost')).toBe(false); + }); + + it('stays off under a parent that does not set it', () => { + render( + + + , + ); + expect(readIgnoreSystemBarsVisibility('inner')).toBe(false); + }); + }); }); const PlainConsumer = React.memo(({ onRender }: { onRender: () => void }) => ( diff --git a/src/__tests__/__snapshots__/SafeAreaContext-test.tsx.snap b/src/__tests__/__snapshots__/SafeAreaContext-test.tsx.snap index 6dbf657d..2fa554fe 100644 --- a/src/__tests__/__snapshots__/SafeAreaContext-test.tsx.snap +++ b/src/__tests__/__snapshots__/SafeAreaContext-test.tsx.snap @@ -2,6 +2,7 @@ exports[`SafeAreaContext does not render child until inset values are received 1`] = ` ; + ignoreSystemBarsVisibility?: WithDefault; } export default codegenNativeComponent( From a20a5aef7e075f0c1e5d412b18e59c8ead8a07b5 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Thu, 5 Nov 2026 00:35:10 +0200 Subject: [PATCH 2/5] feat(android): support ignoreSystemBarsVisibility in SafeAreaProvider --- .../safeareacontext/SafeAreaProvider.kt | 16 +++++- .../SafeAreaProviderManager.kt | 6 +++ .../safeareacontext/SafeAreaUtils.kt | 54 +++++++++++++++---- .../th3rdwave/safeareacontext/SafeAreaView.kt | 6 ++- .../RNCSafeAreaProviderManagerDelegate.java | 8 ++- .../RNCSafeAreaProviderManagerInterface.java | 2 +- 6 files changed, 77 insertions(+), 15 deletions(-) diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt index 1b77db80..409c6492 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProvider.kt @@ -24,12 +24,17 @@ class SafeAreaProvider(context: Context?) : private var mLastFrame: Rect? = null private var mLastCorners: List? = null private val mDispatchedInsets = DispatchedWindowInsets() + private var mIgnoreSystemBarsVisibility = false internal fun getDispatchedWindowInsets(): WindowInsets? = mDispatchedInsets.get(this) + internal fun getIgnoreSystemBarsVisibility(): Boolean = mIgnoreSystemBarsVisibility + private fun maybeUpdateInsets() { val insetsChangeHandler = mInsetsChangeHandler ?: return - val edgeInsets = getSafeAreaInsets(this, getDispatchedWindowInsets()) ?: return + val edgeInsets = + getSafeAreaInsets(this, getDispatchedWindowInsets(), mIgnoreSystemBarsVisibility) + ?: return val frame = getFrame(rootView as ViewGroup, this) ?: return val corners = getRoundedCorners(this) if (mLastInsets != edgeInsets || mLastFrame != frame || mLastCorners != corners) { @@ -72,6 +77,15 @@ class SafeAreaProvider(context: Context?) : return true } + fun setIgnoreSystemBarsVisibility(ignoreSystemBarsVisibility: Boolean) { + if (mIgnoreSystemBarsVisibility == ignoreSystemBarsVisibility) { + return + } + mIgnoreSystemBarsVisibility = ignoreSystemBarsVisibility + maybeUpdateInsets() + invalidate() + } + fun setOnInsetsChangeHandler(handler: OnInsetsChangeHandler?) { mInsetsChangeHandler = handler maybeUpdateInsets() diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt index d440bc46..46036d71 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaProviderManager.kt @@ -5,6 +5,7 @@ import com.facebook.react.module.annotations.ReactModule import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.UIManagerHelper import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.viewmanagers.RNCSafeAreaProviderManagerDelegate import com.facebook.react.viewmanagers.RNCSafeAreaProviderManagerInterface @@ -19,6 +20,11 @@ class SafeAreaProviderManager : public override fun createViewInstance(context: ThemedReactContext) = SafeAreaProvider(context) + @ReactProp(name = "ignoreSystemBarsVisibility") + override fun setIgnoreSystemBarsVisibility(view: SafeAreaProvider, value: Boolean) { + view.setIgnoreSystemBarsVisibility(value) + } + override fun getExportedCustomDirectEventTypeConstants() = mutableMapOf( InsetsChangeEvent.EVENT_NAME to mutableMapOf("registrationName" to "onInsetsChange") diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt index 17eec587..45a006e1 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt @@ -10,14 +10,21 @@ import kotlin.math.max import kotlin.math.min @RequiresApi(Build.VERSION_CODES.R) -private fun getRootWindowInsetsCompatR(windowInsets: WindowInsets?): EdgeInsets? { +private fun getRootWindowInsetsCompatR( + windowInsets: WindowInsets?, + ignoreSystemBarsVisibility: Boolean, +): EdgeInsets? { + if (windowInsets == null) { + return null + } + val types = + WindowInsets.Type.statusBars() or + WindowInsets.Type.displayCutout() or + WindowInsets.Type.navigationBars() or + WindowInsets.Type.captionBar() val insets = - windowInsets?.getInsets( - WindowInsets.Type.statusBars() or - WindowInsets.Type.displayCutout() or - WindowInsets.Type.navigationBars() or - WindowInsets.Type.captionBar() - ) ?: return null + if (ignoreSystemBarsVisibility) windowInsets.getInsetsIgnoringVisibility(types) + else windowInsets.getInsets(types) return EdgeInsets( top = insets.top.toFloat(), right = insets.right.toFloat(), @@ -44,6 +51,19 @@ private fun getRootWindowInsetsCompatM(windowInsets: WindowInsets?): EdgeInsets? ) } +@RequiresApi(Build.VERSION_CODES.M) +@Suppress("DEPRECATION") +private fun getStableWindowInsetsCompatM(windowInsets: WindowInsets?): EdgeInsets? { + val insets = windowInsets ?: return null + val cutout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) insets.displayCutout else null + return EdgeInsets( + top = max(insets.stableInsetTop, cutout?.safeInsetTop ?: 0).toFloat(), + right = max(insets.stableInsetRight, cutout?.safeInsetRight ?: 0).toFloat(), + bottom = max(insets.stableInsetBottom, cutout?.safeInsetBottom ?: 0).toFloat(), + left = max(insets.stableInsetLeft, cutout?.safeInsetLeft ?: 0).toFloat(), + ) +} + private fun getRootWindowInsetsCompatBase(rootView: View): EdgeInsets? { val visibleRect = android.graphics.Rect() rootView.getWindowVisibleDisplayFrame(visibleRect) @@ -58,12 +78,18 @@ private fun getRootWindowInsetsCompatBase(rootView: View): EdgeInsets? { private fun getRootWindowInsetsCompat( rootView: View, dispatchedInsets: WindowInsets?, + ignoreSystemBarsVisibility: Boolean, ): EdgeInsets? { return when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> maxInsets( - getRootWindowInsetsCompatR(rootView.rootWindowInsets), - getRootWindowInsetsCompatR(dispatchedInsets), + getRootWindowInsetsCompatR(rootView.rootWindowInsets, ignoreSystemBarsVisibility), + getRootWindowInsetsCompatR(dispatchedInsets, ignoreSystemBarsVisibility), + ) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ignoreSystemBarsVisibility -> + maxInsets( + getStableWindowInsetsCompatM(rootView.rootWindowInsets), + getStableWindowInsetsCompatM(dispatchedInsets), ) Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> maxInsets( @@ -119,13 +145,19 @@ internal class DispatchedWindowInsets { } @JvmOverloads -fun getSafeAreaInsets(view: View, dispatchedInsets: WindowInsets? = null): EdgeInsets? { +fun getSafeAreaInsets( + view: View, + dispatchedInsets: WindowInsets? = null, + ignoreSystemBarsVisibility: Boolean = false, +): EdgeInsets? { // The view has not been layout yet. if (view.height == 0) { return null } val rootView = view.rootView - val windowInsets = getRootWindowInsetsCompat(rootView, dispatchedInsets) ?: return null + val windowInsets = + getRootWindowInsetsCompat(rootView, dispatchedInsets, ignoreSystemBarsVisibility) + ?: return null // Calculate the part of the view that overlaps with window insets. val windowWidth = rootView.width.toFloat() diff --git a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt index e5c56eb9..8c6917bf 100644 --- a/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt +++ b/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaView.kt @@ -81,7 +81,11 @@ class SafeAreaView(context: Context?) : val dispatchedInsets = if (providerView is SafeAreaProvider) providerView.getDispatchedWindowInsets() else mDispatchedInsets.get(this) - val safeAreaInsets = getSafeAreaInsets(providerView, dispatchedInsets) ?: return false + val ignoreSystemBarsVisibility = + providerView is SafeAreaProvider && providerView.getIgnoreSystemBarsVisibility() + val safeAreaInsets = + getSafeAreaInsets(providerView, dispatchedInsets, ignoreSystemBarsVisibility) + ?: return false val edgeInsets: EdgeInsets if (mCornerAdaptation == CornerAdaptation.NONE) { edgeInsets = safeAreaInsets diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java index f007c96d..15283079 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerDelegate.java @@ -26,6 +26,12 @@ public RNCSafeAreaProviderManagerDelegate(U viewManager) { @Override public void setProperty(T view, String propName, @Nullable Object value) { - super.setProperty(view, propName, value); + switch (propName) { + case "ignoreSystemBarsVisibility": + mViewManager.setIgnoreSystemBarsVisibility(view, value == null ? false : (boolean) value); + break; + default: + super.setProperty(view, propName, value); + } } } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java index 466e74c0..cc8d60af 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNCSafeAreaProviderManagerInterface.java @@ -12,5 +12,5 @@ import android.view.View; public interface RNCSafeAreaProviderManagerInterface { - // No props + void setIgnoreSystemBarsVisibility(T view, boolean value); } From 696fe7ab4f9c3ed65002bbcc71cb07eb5d301987 Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Thu, 5 Nov 2026 00:41:53 +0200 Subject: [PATCH 3/5] feat(ios): support ignoreSystemBarsVisibility in SafeAreaProvider --- ios/Fabric/RNCSafeAreaProviderComponentView.h | 2 ++ .../RNCSafeAreaProviderComponentView.mm | 21 +++++++++++++++ ios/Fabric/RNCSafeAreaViewComponentView.mm | 4 +++ ios/RNCSafeAreaProvider.h | 2 ++ ios/RNCSafeAreaProvider.m | 15 +++++++++++ ios/RNCSafeAreaProviderManager.m | 1 + ios/RNCSafeAreaUtils.h | 2 ++ ios/RNCSafeAreaUtils.m | 26 +++++++++++++++++++ ios/RNCSafeAreaView.m | 3 +++ 9 files changed, 76 insertions(+) diff --git a/ios/Fabric/RNCSafeAreaProviderComponentView.h b/ios/Fabric/RNCSafeAreaProviderComponentView.h index b6cb8e0f..bb1e0790 100644 --- a/ios/Fabric/RNCSafeAreaProviderComponentView.h +++ b/ios/Fabric/RNCSafeAreaProviderComponentView.h @@ -10,6 +10,8 @@ NS_ASSUME_NONNULL_BEGIN @interface RNCSafeAreaProviderComponentView : RCTViewComponentView +@property (nonatomic, readonly) BOOL ignoreSystemBarsVisibility; + extern NSString *const RNCSafeAreaDidChange; @end diff --git a/ios/Fabric/RNCSafeAreaProviderComponentView.mm b/ios/Fabric/RNCSafeAreaProviderComponentView.mm index b17786d6..2f0f0ee9 100644 --- a/ios/Fabric/RNCSafeAreaProviderComponentView.mm +++ b/ios/Fabric/RNCSafeAreaProviderComponentView.mm @@ -106,6 +106,12 @@ - (void)invalidateSafeAreaInsetsMeasuringCorners:(BOOL)measureCorners } UIEdgeInsets verticalCornerAdaptedInsets = _measuredVerticalCornerAdaptedInsets; UIEdgeInsets horizontalCornerAdaptedInsets = _measuredHorizontalCornerAdaptedInsets; + if (_ignoreSystemBarsVisibility) { + CGFloat statusBarTopInset = RNCSafeAreaStatusBarTopInset(self); + safeAreaInsets.top = MAX(safeAreaInsets.top, statusBarTopInset); + verticalCornerAdaptedInsets.top = MAX(verticalCornerAdaptedInsets.top, statusBarTopInset); + horizontalCornerAdaptedInsets.top = MAX(horizontalCornerAdaptedInsets.top, statusBarTopInset); + } #elif TARGET_OS_OSX UIEdgeInsets verticalCornerAdaptedInsets = safeAreaInsets; UIEdgeInsets horizontalCornerAdaptedInsets = safeAreaInsets; @@ -185,9 +191,24 @@ + (ComponentDescriptorProvider)componentDescriptorProvider return concreteComponentDescriptorProvider(); } +- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps +{ + const auto &newProps = *std::static_pointer_cast(props); + BOOL ignoreSystemBarsVisibility = newProps.ignoreSystemBarsVisibility; + BOOL ignoreSystemBarsVisibilityChanged = ignoreSystemBarsVisibility != _ignoreSystemBarsVisibility; + _ignoreSystemBarsVisibility = ignoreSystemBarsVisibility; + + [super updateProps:props oldProps:oldProps]; + + if (ignoreSystemBarsVisibilityChanged) { + [self invalidateSafeAreaInsets]; + } +} + - (void)prepareForRecycle { [super prepareForRecycle]; + _ignoreSystemBarsVisibility = NO; _currentSafeAreaInsets = UIEdgeInsetsZero; _currentVerticalCornerAdaptedInsets = UIEdgeInsetsZero; _currentHorizontalCornerAdaptedInsets = UIEdgeInsetsZero; diff --git a/ios/Fabric/RNCSafeAreaViewComponentView.mm b/ios/Fabric/RNCSafeAreaViewComponentView.mm index 1f90a439..5f93ca76 100644 --- a/ios/Fabric/RNCSafeAreaViewComponentView.mm +++ b/ios/Fabric/RNCSafeAreaViewComponentView.mm @@ -168,6 +168,10 @@ - (void)updateStateIfNecessaryMeasuringCorners:(BOOL)measureCorners } #if TARGET_OS_IPHONE UIEdgeInsets safeAreaInsets = [self providerSafeAreaInsetsMeasuringCorners:measureCorners]; + if ([_providerView isKindOfClass:RNCSafeAreaProviderComponentView.class] && + ((RNCSafeAreaProviderComponentView *)_providerView).ignoreSystemBarsVisibility) { + safeAreaInsets.top = MAX(safeAreaInsets.top, RNCSafeAreaStatusBarTopInset(_providerView)); + } if (UIEdgeInsetsEqualToEdgeInsetsWithThreshold(safeAreaInsets, _currentSafeAreaInsets, 1.0 / RCTScreenScale())) { return; diff --git a/ios/RNCSafeAreaProvider.h b/ios/RNCSafeAreaProvider.h index 5f05c593..c2467cba 100644 --- a/ios/RNCSafeAreaProvider.h +++ b/ios/RNCSafeAreaProvider.h @@ -18,6 +18,8 @@ NS_ASSUME_NONNULL_BEGIN // need to be coalesced before sending, for performance reasons. @property (nonatomic, copy) RCTBubblingEventBlock onInsetsChange; +@property (nonatomic, assign) BOOL ignoreSystemBarsVisibility; + @end NS_ASSUME_NONNULL_END diff --git a/ios/RNCSafeAreaProvider.m b/ios/RNCSafeAreaProvider.m index 7977aa12..6c7a0d8a 100644 --- a/ios/RNCSafeAreaProvider.m +++ b/ios/RNCSafeAreaProvider.m @@ -54,6 +54,15 @@ - (instancetype)initWithEventDispatcher:(id)eventDis return self; } +- (void)setIgnoreSystemBarsVisibility:(BOOL)ignoreSystemBarsVisibility +{ + if (_ignoreSystemBarsVisibility == ignoreSystemBarsVisibility) { + return; + } + _ignoreSystemBarsVisibility = ignoreSystemBarsVisibility; + [self invalidateSafeAreaInsets]; +} + - (void)safeAreaInsetsDidChange { [self invalidateSafeAreaInsetsMeasuringCorners:YES]; @@ -84,6 +93,12 @@ - (void)invalidateSafeAreaInsetsMeasuringCorners:(BOOL)measureCorners } UIEdgeInsets verticalCornerAdaptedInsets = _measuredVerticalCornerAdaptedInsets; UIEdgeInsets horizontalCornerAdaptedInsets = _measuredHorizontalCornerAdaptedInsets; + if (_ignoreSystemBarsVisibility) { + CGFloat statusBarTopInset = RNCSafeAreaStatusBarTopInset(self); + safeAreaInsets.top = MAX(safeAreaInsets.top, statusBarTopInset); + verticalCornerAdaptedInsets.top = MAX(verticalCornerAdaptedInsets.top, statusBarTopInset); + horizontalCornerAdaptedInsets.top = MAX(horizontalCornerAdaptedInsets.top, statusBarTopInset); + } #elif TARGET_OS_OSX NSEdgeInsets safeAreaInsets; if (@available(macOS 11.0, *)) { diff --git a/ios/RNCSafeAreaProviderManager.m b/ios/RNCSafeAreaProviderManager.m index eeeb1b8a..87cb8e68 100644 --- a/ios/RNCSafeAreaProviderManager.m +++ b/ios/RNCSafeAreaProviderManager.m @@ -7,6 +7,7 @@ @implementation RNCSafeAreaProviderManager RCT_EXPORT_MODULE(RNCSafeAreaProvider) RCT_EXPORT_VIEW_PROPERTY(onInsetsChange, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(ignoreSystemBarsVisibility, BOOL) #if TARGET_OS_IPHONE - (UIView *)view diff --git a/ios/RNCSafeAreaUtils.h b/ios/RNCSafeAreaUtils.h index 68502f1d..a2676765 100644 --- a/ios/RNCSafeAreaUtils.h +++ b/ios/RNCSafeAreaUtils.h @@ -35,4 +35,6 @@ typedef struct RNCSafeAreaCornerInputs { RCT_EXTERN RNCSafeAreaCornerInputs RNCSafeAreaCornerInputsMake(UIView *view, UIEdgeInsets safeAreaInsets); RCT_EXTERN BOOL RNCSafeAreaCornerInputsEqual(RNCSafeAreaCornerInputs inputs1, RNCSafeAreaCornerInputs inputs2); + +RCT_EXTERN CGFloat RNCSafeAreaStatusBarTopInset(UIView *view); #endif diff --git a/ios/RNCSafeAreaUtils.m b/ios/RNCSafeAreaUtils.m index 0bd982fc..661563f7 100644 --- a/ios/RNCSafeAreaUtils.m +++ b/ios/RNCSafeAreaUtils.m @@ -77,4 +77,30 @@ BOOL RNCSafeAreaCornerInputsEqual(RNCSafeAreaCornerInputs inputs1, RNCSafeAreaCo CGRectEqualToRect(inputs1.frameInWindow, inputs2.frameInWindow) && CGRectEqualToRect(inputs1.windowFrame, inputs2.windowFrame); } + +CGFloat RNCSafeAreaStatusBarTopInset(UIView *view) +{ +#if TARGET_OS_IOS + if (@available(iOS 13.0, *)) { + UIWindow *window = view.window; + UIStatusBarManager *statusBarManager = window.windowScene.statusBarManager; + if (statusBarManager == nil) { + return 0; + } + // A hidden status bar reports a zero frame, so the last visible height is kept for each vertical size + // class. In landscape on iPhone the status bar is hidden by default, so that height stays 0 until it shows. + static CGFloat statusBarHeights[3]; + UIUserInterfaceSizeClass sizeClass = window.traitCollection.verticalSizeClass; + CGFloat statusBarHeight = CGRectGetHeight(statusBarManager.statusBarFrame); + if (statusBarHeight > 0) { + statusBarHeights[sizeClass] = statusBarHeight; + } else if (statusBarManager.isStatusBarHidden) { + statusBarHeight = statusBarHeights[sizeClass]; + } + CGFloat minY = CGRectGetMinY([view convertRect:view.bounds toView:nil]); + return MAX(statusBarHeight - minY, 0); + } +#endif + return 0; +} #endif diff --git a/ios/RNCSafeAreaView.m b/ios/RNCSafeAreaView.m index 5b494da3..9aa7f3b0 100644 --- a/ios/RNCSafeAreaView.m +++ b/ios/RNCSafeAreaView.m @@ -113,6 +113,9 @@ - (void)invalidateSafeAreaInsets } #if TARGET_OS_IPHONE UIEdgeInsets safeAreaInsets = RNCSafeAreaInsetsWithCornerAdaptation(_providerView, _cornerAdaptation); + if (_providerView.ignoreSystemBarsVisibility) { + safeAreaInsets.top = MAX(safeAreaInsets.top, RNCSafeAreaStatusBarTopInset(_providerView)); + } if (UIEdgeInsetsEqualToEdgeInsetsWithThreshold(safeAreaInsets, _currentSafeAreaInsets, 1.0 / RCTScreenScale())) { return; } From 5b302506cabb96d2596e72fa5c7ad77b7261e4ac Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Thu, 5 Nov 2026 00:42:27 +0200 Subject: [PATCH 4/5] docs: document ignoreSystemBarsVisibility on SafeAreaProvider --- docs/docs/api/safe-area-provider.mdx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/docs/api/safe-area-provider.mdx b/docs/docs/api/safe-area-provider.mdx index 485fe733..ef0a3df4 100644 --- a/docs/docs/api/safe-area-provider.mdx +++ b/docs/docs/api/safe-area-provider.mdx @@ -28,6 +28,14 @@ Optional, defaults to `null`. Can be used to provide the initial value for frame and insets, this allows rendering immediately. See [optimization](/optimizations) for more information on how to use this prop. +#### `ignoreSystemBarsVisibility` + +Optional, defaults to the value of the nearest parent `SafeAreaProvider`, or `false` without one. + +When `true`, the insets keep the space of the status bar and the navigation bar while they are hidden, so content does not jump when the bars hide or show. On Android this uses `getInsetsIgnoringVisibility` on API 30+ and the stable insets on API 23-29. On iOS the top inset keeps the last visible status bar height. On web this prop has no effect. + +A nested `SafeAreaProvider` without this prop uses the value of its parent. Set it to `false` to turn it off for a subtree. + #### `ref` Optional ref to the underlying view. From de2dd8d94920c2dbe6663012eb0383fe121bb0cd Mon Sep 17 00:00:00 2001 From: Bohdan Pomohaibo Date: Thu, 5 Nov 2026 00:43:18 +0200 Subject: [PATCH 5/5] chore(example): add ignoreSystemBarsVisibility example --- example/src/App.tsx | 13 ++- .../src/IgnoreSystemBarsVisibilityExample.tsx | 89 +++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 example/src/IgnoreSystemBarsVisibilityExample.tsx diff --git a/example/src/App.tsx b/example/src/App.tsx index 096e0da8..509a74bf 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -5,6 +5,7 @@ import AdditionalInsetsExample from './AdditionalInsetsExample'; import CornerAdaptationExample from './CornerAdaptationExample'; import FormSheetExample from './FormSheetExample'; import HooksExample from './HooksExample'; +import IgnoreSystemBarsVisibilityExample from './IgnoreSystemBarsVisibilityExample'; import ListenerExample from './ListenerExample'; import SafeAreaViewExample from './SafeAreaViewExample'; import SafeAreaViewEdgesExample from './SafeAreaViewEdgesExample'; @@ -22,7 +23,8 @@ type Example = | 'react-navigation' | 'native-stack' | 'form-sheet' - | 'additional-insets'; + | 'additional-insets' + | 'ignore-system-bars-visibility'; export default function App() { const [currentExample, setCurrentExample] = React.useState( @@ -78,6 +80,12 @@ export default function App() { DevSettings.addMenuItem('Show Additional Insets Example', () => { setCurrentExample('additional-insets'); }); + DevSettings.addMenuItem( + 'Show Ignore System Bars Visibility Example', + () => { + setCurrentExample('ignore-system-bars-visibility'); + }, + ); }, []); let content: React.ReactElement; @@ -109,6 +117,9 @@ export default function App() { case 'additional-insets': content = ; break; + case 'ignore-system-bars-visibility': + content = ; + break; default: content = ( void; +}) { + const insets = useSafeAreaInsets(); + const [statusBarHidden, setStatusBarHidden] = React.useState(false); + + return ( + <> +