Commit 4630ac1
feat(Tabs): add native RTL support for bottom tabs on iOS & Android (#3613)
## 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.
https://github.com/user-attachments/assets/0a96b18b-ddfe-4dd3-99c1-4a08a89ad86d
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](https://github.com/software-mansion/react-native-screens/pull/2185/changes#diff-e5ef5b6e29f17bca80b51bc0c5faef1a44bac24e00952b30ac822520213dc6a5R504)).
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](https://developer.apple.com/documentation/uikit/uiviewcontroller/setoverridetraitcollection(_:forchild:)?language=objc)).
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`](https://developer.apple.com/documentation/uikit/uiviewcontroller/traitoverrides-8u19n?language=objc)
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.
https://github.com/user-attachments/assets/b033d4c7-bfbe-415b-a02c-66fcfed6d0d6
##### 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) |
| --- | --- |
| <video
src="https://github.com/user-attachments/assets/f5a154c0-4e52-40ca-8263-add687c23e65"
/> | <video
src="https://github.com/user-attachments/assets/cddcfab4-90b5-4387-8bea-e9c97c3b29e7"
/>
This bug is reproducible in bare UIKit app on iOS 26.2.
https://github.com/user-attachments/assets/d0a36a07-f6a9-4bfd-9ac9-b5721a70e134
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.
https://github.com/user-attachments/assets/09c25789-b67c-40a1-a3e0-688043cbe17e
##### 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).
<img width="132" height="286" alt="Simulator Screenshot - iPhone 17 Pro
Max - 2026-02-03 at 18 39 51"
src="https://github.com/user-attachments/assets/19082dc4-49c7-433c-8656-37113be18d9a"
/>
### After
#### Android
| Android Test3598 |
| --- |
| <video
src="https://github.com/user-attachments/assets/07fa151c-94b8-465e-892b-af1aa74deb83"
/> |
#### iOS
| iOS Test3598 | iOS TestBottomTabs | iOS Test3288 |
| --- | --- | --- |
| <video
src="https://github.com/user-attachments/assets/6ab88d38-df51-4869-9698-97d1b2ac2079"
/> | <video
src="https://github.com/user-attachments/assets/91d4dffc-c7a3-458c-a973-c61fefc6a2e7"
/> | <video
src="https://github.com/user-attachments/assets/dc775be4-bc07-4a82-8b42-93f54683ba84"
/> |
## Test plan
Use `Test3598`, `TestBottomTabs`, `Test3288` (iOS).
Tested on:
- Android (RTL system language enabled)
- iOS (RTL simulator + device)
## Checklist
- [x] Included code example that can be used to test this change.
- [x] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [ ] Ensured that CI passes
---------
Co-authored-by: Ahmed Awaad <ashahin@aljazirabank.com.sa>
Co-authored-by: Krzysztof Ligarski <krzysztof.ligarski@swmansion.com>1 parent c962866 commit 4630ac1
File tree
17 files changed
+383
-10
lines changed- android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host
- apps/src
- tests/single-feature-tests/tabs
- common/cpp/react/renderer/components/rnscreens
- ios
- conversion
- tabs/host
- src
- components
- helpers
- tabs
- fabric/tabs
17 files changed
+383
-10
lines changedLines changed: 5 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
90 | 95 | | |
91 | 96 | | |
92 | 97 | | |
| |||
Lines changed: 2 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
| 2 | + | |
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| |||
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| 84 | + | |
84 | 85 | | |
85 | 86 | | |
86 | 87 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
9 | 10 | | |
10 | 11 | | |
11 | 12 | | |
| |||
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| 18 | + | |
17 | 19 | | |
18 | 20 | | |
19 | 21 | | |
| |||
Lines changed: 180 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
14 | 31 | | |
Lines changed: 9 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| 24 | + | |
| 25 | + | |
23 | 26 | | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
24 | 33 | | |
25 | 34 | | |
26 | 35 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
484 | 484 | | |
485 | 485 | | |
486 | 486 | | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
487 | 504 | | |
488 | 505 | | |
489 | 506 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
111 | 114 | | |
112 | 115 | | |
113 | 116 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
118 | 118 | | |
119 | 119 | | |
120 | 120 | | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
121 | 145 | | |
122 | 146 | | |
123 | 147 | | |
| |||
161 | 185 | | |
162 | 186 | | |
163 | 187 | | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
164 | 196 | | |
165 | 197 | | |
166 | 198 | | |
0 commit comments