Skip to content

Commit c962866

Browse files
feat(iOS, Tabs): handle colorScheme (dark mode) (#3716)
## Description Adds support for `colorScheme` prop for `TabsHost` on iOS. Color scheme will be propagated down the hierarchy. In follow-up PR, support for this prop will be also added on Android. Closes software-mansion/react-native-screens-labs#982. Supersedes #3681. ### `experimental_userInterfaceStyle` In #3342, we added experimental prop `userInterfaceStyle` to `TabsScreen`. This prop allows handling very specific edge-case when app developer wants to force theme other than system preference for navigation components but keep system color scheme for e.g. keyboard. After some considerations, we decided to keep this property in order not to break users' apps but this property will most likely remain experimental and it will receive limited support because: - there isn't clear API to handle this on the native side, especially on iOS 26+ where we need to rely on internal UIKit view hierarchy to apply `overrideUserInterfaceStyle` in specific place in order for the prop to apply - we think that this is an edge case (if native platform changes keyboard appearance depending on color scheme of the controller, this is the intended behavior) - changing color scheme of keyboard and modals (examples mentioned in the original issue #3162) is possible independently of the container color scheme (https://reactnative.dev/docs/textinput#keyboardappearance-ios, https://reactnative.dev/docs/alert#alertoptions for `react-native`'s `Alert`; we also plan to support overriding color scheme for modals in screens v5) ## Changes - add `colorScheme` prop - handle `colorScheme` for iOS - add single feature test ## Before & after - visual documentation ### Before N/A - tabs would use system's or react-native's color scheme. ### After https://github.com/user-attachments/assets/962b737a-33ae-42d7-8d71-ea753ff49693 ## Test plan Run `single-feature-tests/test-tabs-color-scheme.tsx`. ## Checklist - [x] Included code example that can be used to test this change. - [x] For visual changes, included screenshots / GIFs / recordings documenting the change. - [x] For API changes, updated relevant public types. - [x] Ensured that CI passes --------- Co-authored-by: Ali <ali0fawzish@outlook.com>
1 parent 8ebf028 commit c962866

File tree

10 files changed

+183
-0
lines changed

10 files changed

+183
-0
lines changed

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/host/TabsHostViewManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ class TabsHostViewManager :
8787
value: String?,
8888
) = Unit
8989

90+
override fun setColorScheme(
91+
view: TabsHost?,
92+
value: String?,
93+
) = Unit
94+
9095
@ReactProp(name = "tabBarHidden")
9196
override fun setTabBarHidden(
9297
view: TabsHost,

apps/src/tests/single-feature-tests/tabs/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import OverrideScrollViewContentInsetScenario from './override-scroll-view-conte
55
import TabBarHiddenScenario from './tab-bar-hidden';
66
import TabsScreenOrientationScenario from './tabs-screen-orientation';
77
import TabBarAppearanceDefinedBySelectedTabScenario from './test-tabs-appearance-defined-by-selected-tab';
8+
import TestTabsColorScheme from './test-tabs-color-scheme';
89

910
const scenarios = {
1011
BottomAccessoryScenario,
1112
OverrideScrollViewContentInsetScenario,
1213
TabBarAppearanceDefinedBySelectedTabScenario,
1314
TabBarHiddenScenario,
1415
TabsScreenOrientationScenario,
16+
TestTabsColorScheme,
1517
};
1618

1719
const TabsScenarioGroup: ScenarioGroup<keyof typeof scenarios> = {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {
2+
Appearance,
3+
ColorSchemeName,
4+
ScrollView,
5+
StyleSheet,
6+
Text,
7+
TextInput,
8+
View,
9+
} from 'react-native';
10+
import { Scenario } from '../../shared/helpers';
11+
import { createAutoConfiguredTabs } from '../../shared/tabs';
12+
import React, { useEffect, useState } from 'react';
13+
import { SettingsPicker } from '../../../shared';
14+
import { TabsHostProps } from 'react-native-screens';
15+
import useTabsConfigState from '../../shared/hooks/tabs-config';
16+
17+
const SCENARIO: Scenario = {
18+
name: 'Color Scheme',
19+
key: 'test-tabs-color-scheme',
20+
details: 'Tests how tabs handle system, React Native and prop color scheme.',
21+
platforms: ['ios'],
22+
AppComponent: App,
23+
};
24+
25+
export default SCENARIO;
26+
27+
type TabsParamList = {
28+
Config: undefined;
29+
Keyboard: undefined;
30+
};
31+
32+
function ConfigScreen() {
33+
const [config, dispatch] = useTabsConfigState<TabsParamList>();
34+
const [reactColorScheme, setReactColorScheme] =
35+
useState<ColorSchemeName>('unspecified');
36+
37+
useEffect(() => {
38+
Appearance.setColorScheme(reactColorScheme);
39+
}, [reactColorScheme]);
40+
41+
return (
42+
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
43+
<View style={styles.section}>
44+
<Text>
45+
There are 3 sources of color scheme, in ascending order of precedence:
46+
system, React Native and our property on TabsHost.
47+
</Text>
48+
</View>
49+
50+
<View style={styles.section}>
51+
<Text style={styles.heading}>System color scheme</Text>
52+
<Text>
53+
Switch system color scheme via quick settings in notification drawer
54+
(Android/iOS) or Cmd+Shift+A (iOS simulator).
55+
</Text>
56+
</View>
57+
58+
<View style={styles.section}>
59+
<Text style={styles.heading}>React Native's color scheme</Text>
60+
<SettingsPicker<ColorSchemeName>
61+
label={'colorScheme'}
62+
value={reactColorScheme}
63+
onValueChange={function (value: ColorSchemeName): void {
64+
setReactColorScheme(value);
65+
}}
66+
items={['unspecified', 'light', 'dark']}
67+
/>
68+
</View>
69+
70+
<View style={styles.section}>
71+
<Text style={styles.heading}>TabsHost color scheme</Text>
72+
<SettingsPicker<NonNullable<TabsHostProps['colorScheme']>>
73+
label={'colorScheme'}
74+
value={config.colorScheme ?? 'inherit'}
75+
onValueChange={function (value: TabsHostProps['colorScheme']): void {
76+
dispatch({
77+
type: 'tabBar',
78+
config: {
79+
colorScheme: value,
80+
},
81+
});
82+
}}
83+
items={['inherit', 'light', 'dark']}
84+
/>
85+
</View>
86+
</ScrollView>
87+
);
88+
}
89+
90+
function TestScreen() {
91+
return (
92+
<View style={styles.containerCenter}>
93+
<TextInput placeholder="Type something..." />
94+
</View>
95+
);
96+
}
97+
98+
const Tabs = createAutoConfiguredTabs<TabsParamList>({
99+
Config: ConfigScreen,
100+
Keyboard: TestScreen,
101+
});
102+
103+
export function App() {
104+
return (
105+
<Tabs.Provider>
106+
<Tabs.Autoconfig />
107+
</Tabs.Provider>
108+
);
109+
}
110+
111+
const styles = StyleSheet.create({
112+
container: {
113+
flex: 1,
114+
},
115+
containerCenter: {
116+
flex: 1,
117+
alignItems: 'center',
118+
justifyContent: 'center',
119+
},
120+
content: {
121+
padding: 20,
122+
},
123+
heading: {
124+
fontSize: 24,
125+
fontWeight: 'bold',
126+
marginBottom: 5,
127+
},
128+
section: {
129+
marginBottom: 10,
130+
},
131+
});

ios/conversion/RNSConversions-Tabs.mm

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,4 +484,20 @@ UIUserInterfaceStyle UIUserInterfaceStyleFromTabsScreenCppEquivalent(
484484
}
485485
}
486486

487+
UIUserInterfaceStyle UIUserInterfaceStyleFromHostProp(react::RNSTabsHostColorScheme colorScheme)
488+
{
489+
using enum facebook::react::RNSTabsHostColorScheme;
490+
switch (colorScheme) {
491+
case Inherit:
492+
return UIUserInterfaceStyleUnspecified;
493+
case Light:
494+
return UIUserInterfaceStyleLight;
495+
case Dark:
496+
return UIUserInterfaceStyleDark;
497+
default:
498+
RCTLogError(@"[RNScreens] unsupported color scheme");
499+
return UIUserInterfaceStyleUnspecified;
500+
}
501+
}
502+
487503
}; // namespace rnscreens::conversion

ios/conversion/RNSConversions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ UIInterfaceOrientationMask UIInterfaceOrientationMaskFromRNSOrientation(RNSOrien
108108
RNSOrientation RNSOrientationFromUIInterfaceOrientationMask(UIInterfaceOrientationMask orientationMask);
109109
#endif // !TARGET_OS_TV
110110

111+
UIUserInterfaceStyle UIUserInterfaceStyleFromHostProp(react::RNSTabsHostColorScheme colorScheme);
112+
111113
#pragma mark SplitHost props
112114

113115
UISplitViewControllerSplitBehavior SplitViewPreferredSplitBehaviorFromHostProp(

ios/tabs/host/RNSTabsHostComponentView.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ NS_ASSUME_NONNULL_BEGIN
5454

5555
@property (nonatomic, strong, readonly, nullable) UIColor *nativeContainerBackgroundColor;
5656

57+
@property (nonatomic, readonly) UIUserInterfaceStyle colorScheme;
58+
5759
@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
5860

5961
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)

ios/tabs/host/RNSTabsHostComponentView.mm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ - (void)resetProps
113113
_props = defaultProps;
114114
#endif
115115
_tabBarTintColor = nil;
116+
_colorScheme = UIUserInterfaceStyleUnspecified;
116117
#if !TARGET_OS_TV
117118
_nativeContainerBackgroundColor = [UIColor systemBackgroundColor];
118119
#else // !TARGET_OS_TV
@@ -352,6 +353,11 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
352353
}
353354
}
354355

356+
if (newComponentProps.colorScheme != oldComponentProps.colorScheme) {
357+
_colorScheme = rnscreens::conversion::UIUserInterfaceStyleFromHostProp(newComponentProps.colorScheme);
358+
_controller.overrideUserInterfaceStyle = _colorScheme;
359+
}
360+
355361
// Super call updates _props pointer. We should NOT update it before calling super.
356362
[super updateProps:props oldProps:oldProps];
357363
}

src/components/tabs/TabsHost.types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export type TabsHostNativeContainerStyleProps = {
3030
backgroundColor?: ColorValue;
3131
};
3232

33+
export type TabsHostColorScheme = 'inherit' | 'light' | 'dark';
34+
3335
export interface TabsHostProps {
3436
// #region Events
3537
/**
@@ -151,6 +153,19 @@ export interface TabsHostProps {
151153
* @supported iOS 18 or higher
152154
*/
153155
tabBarControllerMode?: TabBarControllerMode;
156+
/**
157+
* @summary Specifies the color scheme used by the container and any child containers.
158+
*
159+
* The following values are currently supported:
160+
* - `inherit` - the interface style from parent,
161+
* - `light` - the light interface style,
162+
* - `dark` - the dark interface style.
163+
*
164+
* @default inherit
165+
*
166+
* @platform ios
167+
*/
168+
colorScheme?: TabsHostColorScheme;
154169
// #endregion iOS-only
155170

156171
// #region Experimental support

src/components/tabs/TabsScreen.types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,7 @@ export interface TabsScreenProps {
716716
* Does not support dynamic changes to the prop value for the currently visible screen.
717717
*
718718
* Please note that this prop is marked as **experimental** and might be subject to breaking changes or even removal.
719+
* Consider using `colorScheme` on `TabsHost` instead.
719720
*
720721
* The following values are currently supported:
721722
* - `unspecified` - an unspecified interface style,

src/fabric/tabs/TabsHostNativeComponent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type TabBarMinimizeBehavior =
1616

1717
type TabBarControllerMode = 'automatic' | 'tabBar' | 'tabSidebar';
1818

19+
type TabsHostColorScheme = 'inherit' | 'light' | 'dark';
20+
1921
export interface NativeProps extends ViewProps {
2022
// Events
2123
onNativeFocusChange?: CT.DirectEventHandler<NativeFocusChangeEvent>;
@@ -28,6 +30,7 @@ export interface NativeProps extends ViewProps {
2830
tabBarTintColor?: ColorValue;
2931
tabBarMinimizeBehavior?: CT.WithDefault<TabBarMinimizeBehavior, 'automatic'>;
3032
tabBarControllerMode?: CT.WithDefault<TabBarControllerMode, 'automatic'>;
33+
colorScheme?: CT.WithDefault<TabsHostColorScheme, 'inherit'>;
3134

3235
// Control
3336

0 commit comments

Comments
 (0)