feat(Tabs): add native RTL support for bottom tabs on iOS & Android#3613
Conversation
…ntroller mode handling
|
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. |
|
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. |
|
@kligarski sure thank you :D |
|
@kkafar welcome , any time :) |
kligarski
left a comment
There was a problem hiding this comment.
Leaving some comments for the reviewers.
| } | ||
|
|
||
| if (newComponentProps.directionMode != oldComponentProps.directionMode) { | ||
| _directionMode = |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
@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.
t0maboro
left a comment
There was a problem hiding this comment.
leaving 1 comment, I haven't verified the runtime yet
There was a problem hiding this comment.
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.
kmichalikk
left a comment
There was a problem hiding this comment.
Only some minor things related to the code. Runtime appears to be working. Good job!
There was a problem hiding this comment.
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
directionprop onTabsHostand wires it to Android viastyle.directionand 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.
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
directionprop directly toTabsHostview.iOS
Badges
Setting
semanticContentAttributefor_controller.tabBarand_controller.viewis 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'sappearanceWhenContainedInInstancesOfClassesof 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._UITabContentViewis mounted undercontroller.view. UsingappearanceWhenContainedInInstancesOfClassesfor_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-screensbut I think that this is the lesser evil. If this turns out to be problematic, we can consider introducing some kind ofScreensRootViewthat will ensure that there is a top controller fromscreens.For iOS 17+, we can use
traitOverridesdirectly 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 nextreact-nativerelease.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).
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-nativeon iOSImportant
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 ofsemanticContentAttribute(used by regularreact-nativeviews) which should only define whether the view should be flipped in RTL & does not propagate down the hierarchy.forceRTLdoes not change the trait therefore containers use layout direction of the native app. In order to supportforceRTL, you should usedirection={I18nManager.isRTL ? 'rtl' : 'ltr'}(see our example implementation inBottomTabsContainer.tsx). This will override the trait from the app with layout direction fromreact-nativeand propagate it down the hierarchy.Changes
directionprop toTabsHostand implement it for both platformsTest3598.tsxBefore
(iOS only because Android works out of the box).

After
Android
android_3613.mp4
iOS
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:
Checklist