Skip to content

feat(Tabs): add native RTL support for bottom tabs on iOS & Android#3613

Merged
kligarski merged 31 commits intosoftware-mansion:mainfrom
ahmedawaad1804:add-bottomtab-rtl-support
Mar 4, 2026
Merged

feat(Tabs): add native RTL support for bottom tabs on iOS & Android#3613
kligarski merged 31 commits intosoftware-mansion:mainfrom
ahmedawaad1804:add-bottomtab-rtl-support

Conversation

@ahmedawaad1804
Copy link
Copy Markdown
Contributor

@ahmedawaad1804 ahmedawaad1804 commented Feb 3, 2026

Description

This PR adds proper RTL (Right-to-Left) layout support for native bottom tabs on both iOS and Android in react-native-screens.

Previously, bottom tabs did not support the RTL direction, which caused incorrect tab ordering and layout in RTL locales (e.g. Arabic, Hebrew).

With this change bottom tabs now support changing layout direction. This brings native behavior in line with expected platform RTL handling and React Native layout conventions.

Details

@kligarski:

Android

On Android, the direction works out-of-the-box as it's propagated through view hierarchy. We pass the value of the direction prop directly to TabsHost view.

iOS

Badges

Setting semanticContentAttribute for _controller.tabBar and _controller.view is not enough, e.g. badges visible through liquid glass lens are still in LTR.

Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.16.27.18.mov

To handle this, we tried using the same approach as in native stack - we set UIView's appearanceWhenContainedInInstancesOfClasses of the tab bar (details how it's handled in the header are here).

However, this does not work for tab bar & sidebar on iPad starting from iOS 18 as it is not a part of controller.tabBar. _UITabContentView is mounted under controller.view. Using appearanceWhenContainedInInstancesOfClasses for _UITabContentView (which is already sketchy as this is an internal UIKit class) helps with the order of items in the tab bar but the sidebar appears on the wrong side of the screen.

That's why I decided to use modern way to handle direction via trait overrides. For iOS prior to 17, you need to apply overrides on parent view controller (see here). This isn't the cleanest solution as the controller changes property of the other controller which might not be a controller belonging to react-native-screens but I think that this is the lesser evil. If this turns out to be problematic, we can consider introducing some kind of ScreensRootView that will ensure that there is a top controller from screens.

For iOS 17+, we can use traitOverrides directly on the controller - this works with the top tab bar/sidebar on iPadOS 18+.

ScrollView

On iOS, there is a bug with content of the ScrollView being moved off screen after tab changes. I've reported the issue to react-native: facebook/react-native#55768. The fix has been merged (facebook/react-native#55804) and should be available in next react-native release.

Screen.Recording.2026-02-19.at.15.55.06.mov
Bottom Accessory

There seems to be a bug with bottom accessory in RTL when search role is NOT used for one of the tabs (Apple Music and Apple Podcasts use search role so the bug isn't visible).

no search role (bug) search role (no bug)
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.52.15.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.51.31.mov

This bug is reproducible in bare UIKit app on iOS 26.2.

Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.16.19.15.mov

I've added this to our internal board (https://github.com/software-mansion/react-native-screens-labs/issues/986) and we'll check whether it has been fixed in iOS 26.3/26.4 beta.

Top tab bar badges

On iPadOS 18+, there seems to be a bug with the initial position of the badges. They move to correct position after a tab change. I added a ticket on our internal board to check whether this is a native bug: https://github.com/software-mansion/react-native-screens-labs/issues/991.

Simulator.Screen.Recording.-.iPad.Pro.13-inch.M5.-.2026-02-23.at.09.47.57.mov
Native localization vs react-native on iOS

Important

When RTL is forced via I18nManager.forceRTL(true) but the language of the native app isn't an RTL language, the views related to containers such as the tab bar/sidebar by default will remain in LTR on iOS. This is because we want to rely on native mechanism for layout direction which is the trait system instead of semanticContentAttribute (used by regular react-native views) which should only define whether the view should be flipped in RTL & does not propagate down the hierarchy. forceRTL does not change the trait therefore containers use layout direction of the native app. In order to support forceRTL, you should use direction={I18nManager.isRTL ? 'rtl' : 'ltr'} (see our example implementation in BottomTabsContainer.tsx). This will override the trait from the app with layout direction from react-native and propagate it down the hierarchy.

Changes

  • add direction prop to TabsHost and implement it for both platforms
  • add Test3598.tsx

Before

(iOS only because Android works out of the box).
Simulator Screenshot - iPhone 17 Pro Max - 2026-02-03 at 18 39 51

After

Android

Android Test3598
android_3613.mp4

iOS

iOS Test3598 iOS TestBottomTabs iOS Test3288
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.49.45.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.50.11.mov
Simulator.Screen.Recording.-.iPhone.17.Pro.RTL.-.2026-02-19.at.15.51.31.mov

Test plan

Use Test3598, TestBottomTabs, Test3288 (iOS).

Tested on:

  • Android (RTL system language enabled)
  • iOS (RTL simulator + device)

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • Ensured that CI passes

@kkafar
Copy link
Copy Markdown
Member

kkafar commented Feb 16, 2026

Hey, thanks for the PR. This is great, we need it. I hope to review it somewhere this week. Will keep you up to date.

@kligarski kligarski self-assigned this Feb 17, 2026
@kligarski
Copy link
Copy Markdown
Contributor

Hi, thank you for the PR. I'll push some changes and update PR description so we can land it soon. I hope you don't mind.

@ahmedawaad1804
Copy link
Copy Markdown
Contributor Author

@kligarski sure thank you :D

@ahmedawaad1804
Copy link
Copy Markdown
Contributor Author

@kkafar welcome , any time :)

Copy link
Copy Markdown
Contributor

@kligarski kligarski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving some comments for the reviewers.

}

if (newComponentProps.directionMode != oldComponentProps.directionMode) {
_directionMode =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use [RCTI18nUtil isRTL] instead of passing a prop, it should have an updated value as it reads it from [NSUserDefaults standardUserDefaults] (but it would be unable to react to dynamic changes). I'm not sure if we can consider this a stable API but it has been mentioned in blog post in react-native: https://reactnative.dev/blog/2016/08/19/right-to-left-support-for-react-native-apps.

cc @kkafar - let me know what you think is better

We can also leave the prop for now (it's not exposed as a part of API either way) and rethink our approach to RTL in separate PR - I'm wondering whether RTL should be handled via some top-level wrapper component for the entire hierarchy below it.

Copy link
Copy Markdown
Contributor

@satya164 satya164 Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kligarski in react navigation, we have a direction prop on NavigationContainer to control layout direction which we will pass it to navigators that can handle it.

@kligarski kligarski changed the title Add native RTL support for bottom tabs on iOS & Android with optional direction override feat(Tabs): add native RTL support for bottom tabs on iOS & Android Feb 23, 2026
Copy link
Copy Markdown
Contributor

@t0maboro t0maboro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

leaving 1 comment, I haven't verified the runtime yet

Comment thread ios/bottom-tabs/host/RNSBottomTabsHostComponentView.mm Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This util was useful in previous version of this PR but now it isn't really used. It's out of scope of this PR but if you don't mind, I'd keep it as it makes the code cleaner.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kligarski sure

@kligarski kligarski requested a review from t0maboro March 2, 2026 18:28
Copy link
Copy Markdown
Contributor

@kmichalikk kmichalikk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only some minor things related to the code. Runtime appears to be working. Good job!

Comment thread ios/conversion/RNSConversions-SplitView.mm Outdated
Comment thread ios/tabs/host/RNSTabBarController.h
Comment thread ios/tabs/host/RNSTabBarController.h
Comment thread ios/tabs/host/RNSTabBarController.h
Comment thread ios/tabs/host/RNSTabBarController.h Outdated
Comment thread ios/tabs/host/RNSTabBarController.mm Outdated
Comment thread ios/tabs/host/RNSTabBarController.mm Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a configurable layout-direction API for native bottom tabs to properly support RTL on both iOS and Android, aligning native tab ordering/layout with React Native expectations (including cases like I18nManager.forceRTL on iOS).

Changes:

  • Introduces a new direction prop on TabsHost and wires it to Android via style.direction and to iOS via trait overrides.
  • Adds iOS native plumbing to propagate layout direction (including an iOS < 17 fallback path).
  • Adds a new example/test scenario to validate direction behavior across system/RN/prop sources.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/fabric/tabs/TabsHostNativeComponent.ts Adds native layoutDirection prop (codegen-facing) with default 'inherit'.
src/components/tabs/TabsHost.types.ts Adds public direction prop and documents platform-specific behavior.
src/components/tabs/TabsHost.tsx Maps direction to Android style.direction and iOS layoutDirection; refactors iOS 26 gating.
src/components/helpers/PlatformUtils.ts Introduces isIOS26OrHigher helper used for iOS 26 feature gating.
src/components/ScreenStackItem.tsx Uses isIOS26OrHigher helper instead of inline Platform.Version checks.
ios/tabs/host/RNSTabsHostComponentView.mm Stores/updates layout direction prop and applies iOS 17+ trait override or schedules iOS < 17 update.
ios/tabs/host/RNSTabsHostComponentView.h Exposes layoutDirection from host view to the controller.
ios/tabs/host/RNSTabBarController.mm Adds iOS < 17 trait override update path and triggers it on attach-to-parent.
ios/tabs/host/RNSTabBarController.h Declares layout-direction update APIs + dirty flag for iOS < 17.
ios/conversion/RNSConversions.h Declares conversion helper for the new tabs host layout direction enum.
ios/conversion/RNSConversions-Tabs.mm Implements conversion from C++ enum to UIKit layout direction.
common/cpp/react/renderer/components/rnscreens/RNSTabsBottomAccessoryShadowNode.h Adds a layout() override hook to apply RTL frame corrections.
common/cpp/react/renderer/components/rnscreens/RNSTabsBottomAccessoryShadowNode.cpp Forces x-origin to 0 to keep bottom accessory offset logic consistent in RTL.
apps/src/tests/single-feature-tests/tabs/test-tabs-layout-direction.tsx Adds a dedicated scenario for testing system/RN/prop direction interactions.
apps/src/tests/single-feature-tests/tabs/index.ts Registers the new tabs layout direction scenario.
apps/src/shared/gamma/containers/bottom-tabs/BottomTabsContainer.tsx Passes explicit direction based on I18nManager.isRTL to support forceRTL cases on iOS.
android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt Adds a no-op setter to satisfy the generated interface for the new prop on Android.
Comments suppressed due to low confidence (1)

apps/src/tests/single-feature-tests/tabs/test-tabs-layout-direction.tsx:67

  • Typo in user-facing text: "propery" should be "property".
          There are 3 sources of layout direction: system, React Native and our
          propery on TabsHost.
        </Text>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ios/tabs/host/RNSTabBarController.mm
Comment thread ios/tabs/host/RNSTabBarController.mm
Comment thread ios/conversion/RNSConversions-Tabs.mm
@kligarski kligarski merged commit 4630ac1 into software-mansion:main Mar 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants