Skip to content

Support a mirrored horizontal inset for content centered beside a one-sided safe area #810

Closed as not planned

Description

@kockar96

On the iPhone Duo cover display, the control column gives the window a one-sided safe area: 84 pt on the right and 0 on the left, in a 466 x 678 pt window. Apps that want content centered on the display have to mirror that inset on the left, and today they can only do it from JS with useSafeAreaInsets().

That is too late during a fold or unfold. iOS keeps showing the first frame committed for the new display through the transition, and JS is busy at that moment. In our app, JS insets reached the new display about a second after its first frame, so content appeared in the wrong place and then shifted, by 35 pt on one measured screen. Since #803, SafeAreaView applies native insets in time, but it has no way to express a mirrored inset.

Proposal

Add a way for SafeAreaView to apply the wider horizontal inset on both sides, resolved natively in the same layout pass.

We currently patch this in as a mirrored value for the left and right edge modes:

<SafeAreaView
  edges={{ top: 'additive', left: 'mirrored', right: 'mirrored' }}
  style={{ paddingHorizontal: 16 }}
/>
--- a/common/cpp/react/renderer/components/safeareacontext/RNCSafeAreaViewShadowNode.cpp
+++ b/common/cpp/react/renderer/components/safeareacontext/RNCSafeAreaViewShadowNode.cpp
-  left = Style::Length::points(getEdgeValue(
-      edges.left, stateData.insets.left, left.value().unwrapOrDefault(0)));
-  right = Style::Length::points(getEdgeValue(
-      edges.right, stateData.insets.right, right.value().unwrapOrDefault(0)));
+  auto narrowerInset = fmin(stateData.insets.left, stateData.insets.right);
+  auto widerInset = fmax(stateData.insets.left, stateData.insets.right);
+  left = Style::Length::points(
+      edges.left == "mirrored"
+          ? fmax(widerInset - left.value().unwrapOrDefault(0), narrowerInset)
+          : getEdgeValue(
+                edges.left,
+                stateData.insets.left,
+                left.value().unwrapOrDefault(0)));
+  right = Style::Length::points(
+      edges.right == "mirrored"
+          ? fmax(widerInset - right.value().unwrapOrDefault(0), narrowerInset)
+          : getEdgeValue(
+                edges.right,
+                stateData.insets.right,
+                right.value().unwrapOrDefault(0)));

Each edge gets the wider inset less its style padding, but never less than the narrower inset. The style padding is treated as padding the content keeps inside, so with insets of 0 and 84 and a padding of 16, both edges resolve to 68. With equal insets of 0, as on an ordinary portrait iPhone, both resolve to 0.

Evidence

Measured with this patch on 5.11.1 and React Native 0.88.0, on the iPhone Duo simulator in Xcode 27.2. We ran 18 opens and 18 closes across three screens and compared every recorded frame with the final one. None differed visibly in layout: the worst was 536 of about 145,000 compared pixels, with the status bar area excluded.

Limits of this evidence:

  • We have not run it on a physical iPhone Duo.
  • We have not run it on an ordinary iPhone.
  • It is iOS only. Android's SafeAreaViewEdgeModes.valueOf rejects mirrored, so a real implementation needs an Android side.
  • We don't have a public repro yet, but can make one if that helps.

Question

Would you accept this, and in what shape? The style padding behavior above is what our layout needed, but it differs from the other edge modes, so a separate prop or different semantics may fit the library better. If the direction works for you, we can send a PR.

Activity

  1. kockar96 commented on Nov 5, 2026

    @kockar96
    Author

    Closing this. Further measurement shows that mirroring the inset in JS behaves the same, because the insets are identical on both displays of the device I was testing. The shift I described did not come from this. Sorry for the noise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions