Skip to content

[iOS][Fabric] Text inside Modal keeps its old size after a live text size change (works on 0.86.3) #59375

Closed

Description

@simongt

Description

On iOS with the New Architecture, Text inside a <Modal> doesn't follow a live system text size change. If the text size goes up while the app is open, every Text in the modal keeps the old size, while the TextInputs update. Relaunching fixes it. The same app on 0.86.3 updates correctly.

We hit this in our app through react-native-screens (expo-router native stack), whose screens take the same path, on the simulator and on an iPhone 16 Pro Max with iOS 26.7.1. There a later commit happens to repair the increase, but going from XXXL back to the default size leaves the screen large until it remounts.

What I found with logging in YogaLayoutableShadowNode on 0.88.0:

I haven't bisected 0.87, but #57246 moved the font scale handling into configureYogaTree, which is where this dirty mark starts. Related: #57512. I'm looking at a fix and will link a PR here.

Steps to reproduce

  1. Clone the reproducer, run npm install, then cd ios && bundle install && bundle exec pod install.
  2. Run a Release build on an iOS simulator at the default text size.
  3. With the app open, run xcrun simctl ui <udid> content_size extra-extra-extra-large (or change Settings > Accessibility > Display & Text Size > Larger Text).
  4. The text in the modal keeps its old size. Only the TextInputs grow.
  5. Relaunch the app at the same size. Now it's correct.

scripts/capture.sh <udid> runs steps 2 to 5 and saves the screenshots.

React Native Version

0.88.0

Affected Platforms

Runtime - iOS

Output of npx @react-native-community/cli info

System:
  OS: macOS 26.7.1
  CPU: (14) arm64 Apple M4 Pro
  Memory: 2.79 GB / 48.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 22.21.1
    path: /Users/simon/.nvm/versions/node/v22.21.1/bin/node
  Yarn:
    version: 1.22.22
    path: /Users/simon/.nvm/versions/node/v22.21.1/bin/yarn
  npm:
    version: 10.9.4
    path: /Users/simon/.nvm/versions/node/v22.21.1/bin/npm
  Watchman:
    version: 2026.05.25.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.16.2
    path: /Users/simon/.rbenv/shims/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 27.0
      - iOS 27.1
      - macOS 27.0
      - tvOS 27.0
      - visionOS 27.0
      - watchOS 27.0
  Android SDK: Not Found
IDEs:
  Android Studio: 2025.3 AI-253.32098.37.2534.15336583
  Xcode:
    version: 27.1/27A9275
    path: /usr/bin/xcodebuild
Languages:
  Java:
    version: 17.0.19
    path: /usr/bin/javac
  Ruby:
    version: 3.2.2
    path: /Users/simon/.rbenv/shims/ruby
npmPackages:
  "@react-native-community/cli":
    installed: 20.2.0
    wanted: 20.2.0
  react:
    installed: 19.3.0
    wanted: 19.3.0
  react-native:
    installed: 0.88.0
    wanted: 0.88.0
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: true
iOS:
  hermesEnabled: true
  newArchEnabled: true

Stacktrace or Logs

No crash, layout only. Trimmed excerpt from the logging I added to YogaLayoutableShadowNode.cpp, on the live change to XXXL. "stored" is layoutMetrics.fontSizeMultiplier, "chain" is the Paragraph's Yoga owners nearest first, and * marks a dirty Yoga node.

pass root=RootView#11 stored=1.000 new=1.353
dirty node=Paragraph#4 stored=1.000 new=1.353 chain=View View ModalHostView* View RootView*
nolayout parent=View#34 child=ModalHostView#32 childStored=1.000 new=1.353 staleTextNodes=7

MANDATORY Reproducer

https://github.com/simongt/rn-modal-text-size-repro

Screenshots and Videos

Version Default size Live change to XXXL Relaunch at XXXL
0.88.0
0.86.3

iPhone 17 Pro simulator, iOS 26.5, Release builds with the default prebuilt core.

Activity

  1. r1shabhsharma05 commented on Oct 22, 2026

    @r1shabhsharma05

    /assign

  2. simongt commented on Oct 23, 2026

    @simongt
    ContributorAuthor

    Fix is up.

    After @javache's review, it now works inside the pass that already pushes the new font scale down the tree: as that pass unwinds, a view marks itself dirty when a child it just updated came back dirty, the same rule layout already follows when a parent is rebuilt. Hidden views are skipped, and it's still behind the font scale flag.


    @lazerg also opened this, with the same idea applied at the point where the child gets copied. Flagging it here so the two don't duplicate effort.

  3. lazerg commented on Oct 23, 2026

    @lazerg
    Contributor

    Closed mine

  4. kabal1000x commented on Oct 25, 2026

    @kabal1000x

    Same symptom without a <Modal>, on 0.88.0 iOS (Fabric), in an Expo SDK 58 app (React 19.3.0), on iPhone SE (3rd gen) and iPhone 17 Pro Max simulators, iOS 26.3. The screen is a plain stack screen (Expo Router / react-native-screens 4.30.0) holding a few Text and one TextInput with a placeholder. After switching Larger Text from the default to the largest accessibility size while the screen is mounted, every Text keeps its old size (checked at 1 s and 6 s), and the TextInput placeholder draws at the new size inside its unchanged box. A freshly mounted screen at the same setting lays out correctly. Mounted text catches up only when something else commits a layout change.

    Reading the 0.88.0 sources, there is a gap before configureYogaTree is ever reached, which #59383 would not cover:

    1. RCTFabricSurface observes UIContentSizeCategoryDidChangeNotification and calls _updateLayoutContext, which sets layoutContext.fontSizeMultiplier = RCTFontSizeMultiplier() and calls _surfaceHandler->constraintLayout(layoutConstraints, layoutContext). The constraints are unchanged.
    2. SurfaceHandler::constraintLayout commits oldRootShadowNode.clone(propsParserContext, layoutConstraints, layoutContext) and nothing else.
    3. RootShadowNode::clone dirties only on a constraints change:
      if (layoutConstraints != getConcreteProps().layoutConstraints) {
        newRootShadowNode->dirtyLayout();
      }
    4. RootShadowNode::layoutIfNeeded returns early when getIsLayoutClean(), so layoutTree → configureYogaTree (where the enableFontScaleChangesUpdatingLayout check now lives since Fix Fabric reusing nodes with a stale font scale (#57246) #57246) never runs for this commit.

    Before #57246, SurfaceHandler::constraintLayout on 0.86-stable called dirtyMeasurableNodes(*newRoot) when fontSizeMultiplier changed. #57246 removed that call and the helpers. main still has the 0.88.0 RootShadowNode::clone and SurfaceHandler::constraintLayout.

    #57124 (open, labelled Android) adds exactly the missing step in shared code, newRoot->dirtyLayout() when fontSizeMultiplier changes, so it looks relevant to iOS too, and possibly needed alongside #59383. The general report for this symptom is #45655.

    Screenshots (before / mounted after change / fresh mount) available; can supply a minimal repro on request.

  5. simongt commented on Oct 28, 2026

    @simongt
    ContributorAuthor

    Quick update: @javache approved #59383, so it should land once it makes it through Meta's import. The font scale flag also came off main in #59389, so the fix isn't behind a flag anymore.


    @kabal1000x thanks for digging into this. Your stack screen case looks like the same bug: react-native-screens' screens set their own size when they're cloned, the same way the modal does, and that's the path #59383 fixes. I checked it on an expo-router stack screen on 0.88.0 built from source, and with the change a live switch to XXXL and back matches a fresh mount.

    On the root: it does get dirty on a font-only change, just not through the check in RootShadowNode::clone. Cloning the root with a new font scale rebuilds its props from scratch, so its style loses the min and max size that layout writes straight into Yoga, and that mismatch marks it dirty. So layout does run. I logged that pass in a modal repro and in the stack screen app, and the tests in #59383 clone the root with only a new font scale and expect layoutIfNeeded() to return true.

    You're right that it's a side effect rather than an explicit rule though, so #57124 dirtying the root on a font scale change would be good alongside. On its own it only dirties the root, so the dirty mark from the text would still stop at the modal or screen on its way up.

    Screenshots (before / mounted after change / fresh mount) available; can supply a minimal repro on request.

    Could you share the minimal repro? I haven't tried the largest accessibility size on a stack screen yet, so I'll run yours with #59383 applied.

  6. added a commit that references this issue on Oct 29, 2026
    e8e63cf
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