Skip to content

Commit 7d97100

Browse files
Saadnajmiclaude
andcommitted
Fix Fabric colors not respecting macOS dark mode appearance
Resolve dynamic/semantic colors against the current effective appearance on macOS so that dark mode colors are correctly extracted in the Fabric renderer. Fixes microsoft#2830 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2a222b2 commit 7d97100

5 files changed

Lines changed: 212 additions & 11 deletions

File tree

packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,18 @@ - (void)invalidateLayer
11771177
#if !TARGET_OS_OSX // [macOS]
11781178
RCTPlatformColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection];
11791179
#else // [macOS
1180+
// Resolve dynamic/semantic colors against the current effective appearance
1181+
// so that dark mode colors are correctly applied.
11801182
RCTPlatformColor *backgroundColor = _backgroundColor;
1183+
if (_backgroundColor) {
1184+
NSAppearance *previousAppearance = NSAppearance.currentAppearance;
1185+
NSAppearance.currentAppearance = self.effectiveAppearance ?: [NSApp effectiveAppearance];
1186+
NSColor *resolved = [_backgroundColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
1187+
NSAppearance.currentAppearance = previousAppearance;
1188+
if (resolved) {
1189+
backgroundColor = resolved;
1190+
}
1191+
}
11811192
#endif // macOS]
11821193
// The reason we sometimes do not set self.layer's backgroundColor is because
11831194
// we want to support non-uniform border radii, which apple does not natively

packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,21 @@ int32_t ColorFromColorComponents(const facebook::react::ColorComponents &compone
120120
int32_t ColorFromUIColor(RCTPlatformColor *color) // [macOS]
121121
{
122122
CGFloat rgba[4];
123+
#if TARGET_OS_OSX // [macOS
124+
// Resolve dynamic/semantic colors against the current effective appearance
125+
// so that dark mode colors are correctly extracted.
126+
NSAppearance *previousAppearance = NSAppearance.currentAppearance;
127+
NSAppearance.currentAppearance = [NSApp effectiveAppearance];
128+
NSColor *resolvedColor = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
129+
if (resolvedColor) {
130+
[resolvedColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
131+
} else {
132+
[color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
133+
}
134+
NSAppearance.currentAppearance = previousAppearance;
135+
#else // macOS]
123136
[color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
137+
#endif
124138
return ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]});
125139
}
126140

@@ -170,9 +184,33 @@ int32_t ColorFromUIColor(const std::shared_ptr<void> &uiColor)
170184
return 0;
171185
}
172186

173-
#if TARGET_OS_OSX // [macOS]
174-
return ColorFromUIColor(uiColor);
175-
#else // [macOS
187+
#if TARGET_OS_OSX // [macOS
188+
// Hash both light and dark appearance colors to properly distinguish
189+
// dynamic colors that change with appearance.
190+
RCTPlatformColor *color = (RCTPlatformColor *)unwrapManagedObject(uiColor);
191+
int32_t darkColor = 0;
192+
int32_t lightColor = 0;
193+
NSAppearance *previousAppearance = NSAppearance.currentAppearance;
194+
195+
NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
196+
NSColor *darkResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
197+
if (darkResolved) {
198+
CGFloat rgba[4];
199+
[darkResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
200+
darkColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]});
201+
}
202+
203+
NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua];
204+
NSColor *lightResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
205+
if (lightResolved) {
206+
CGFloat rgba[4];
207+
[lightResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
208+
lightColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]});
209+
}
210+
211+
NSAppearance.currentAppearance = previousAppearance;
212+
return facebook::react::hash_combine(darkColor, lightColor);
213+
#else // macOS]
176214
static UITraitCollection *darkModeTraitCollection =
177215
[UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
178216
auto darkColor = ColorFromUIColorForSpecificTraitCollection(uiColor, darkModeTraitCollection);

packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,17 @@
184184
{
185185
CGFloat rgba[4];
186186
#if TARGET_OS_OSX // [macOS
187-
color = [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]];
188-
#endif // macOS]
187+
// Resolve dynamic/semantic colors against the current effective appearance
188+
// so that dark mode colors are correctly extracted.
189+
NSAppearance *previousAppearance = NSAppearance.currentAppearance;
190+
NSAppearance.currentAppearance = [NSApp effectiveAppearance];
191+
NSColor *resolvedColor = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]];
192+
NSAppearance.currentAppearance = previousAppearance;
193+
NSColor *finalColor = resolvedColor ?: [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]];
194+
[finalColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
195+
#else // macOS]
189196
[color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]];
197+
#endif
190198
return {(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]};
191199
}
192200

packages/rn-tester/js/examples/Playground/RNTesterPlayground.js

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,121 @@ import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
1212

1313
import RNTesterText from '../../components/RNTesterText';
1414
import * as React from 'react';
15-
import {StyleSheet, View} from 'react-native';
15+
import {Platform, PlatformColor, StyleSheet, View} from 'react-native';
16+
import {DynamicColorMacOS} from 'react-native'; // [macOS]
17+
18+
function ColorSwatch({
19+
color,
20+
label,
21+
}: {
22+
color: ReturnType<typeof PlatformColor>,
23+
label: string,
24+
}) {
25+
return (
26+
<View style={styles.row}>
27+
<View style={[styles.swatch, {backgroundColor: color}]} />
28+
<RNTesterText style={styles.label}>{label}</RNTesterText>
29+
</View>
30+
);
31+
}
1632

1733
function Playground() {
34+
if (Platform.OS !== 'macos') {
35+
return (
36+
<View style={styles.container}>
37+
<RNTesterText>This test is macOS-only.</RNTesterText>
38+
</View>
39+
);
40+
}
41+
1842
return (
1943
<View style={styles.container}>
20-
<RNTesterText>
21-
Edit "RNTesterPlayground.js" to change this file
44+
<RNTesterText style={styles.heading}>
45+
Fabric Dark Mode Color Test
46+
</RNTesterText>
47+
<RNTesterText style={styles.description}>
48+
These colors should change when you toggle between Light and Dark
49+
appearance in System Settings. If they all look the same in both modes,
50+
the bug is not fixed.
51+
</RNTesterText>
52+
53+
<RNTesterText style={styles.sectionTitle}>System Colors</RNTesterText>
54+
<ColorSwatch
55+
color={PlatformColor('systemBlueColor')}
56+
label="systemBlueColor"
57+
/>
58+
<ColorSwatch
59+
color={PlatformColor('systemRedColor')}
60+
label="systemRedColor"
61+
/>
62+
<ColorSwatch
63+
color={PlatformColor('systemGreenColor')}
64+
label="systemGreenColor"
65+
/>
66+
<ColorSwatch
67+
color={PlatformColor('systemOrangeColor')}
68+
label="systemOrangeColor"
69+
/>
70+
71+
<RNTesterText style={styles.sectionTitle}>Semantic Colors</RNTesterText>
72+
<ColorSwatch
73+
color={PlatformColor('labelColor')}
74+
label="labelColor"
75+
/>
76+
<ColorSwatch
77+
color={PlatformColor('secondaryLabelColor')}
78+
label="secondaryLabelColor"
79+
/>
80+
<ColorSwatch
81+
color={PlatformColor('windowBackgroundColor')}
82+
label="windowBackgroundColor"
83+
/>
84+
<ColorSwatch
85+
color={PlatformColor('controlBackgroundColor')}
86+
label="controlBackgroundColor"
87+
/>
88+
<ColorSwatch
89+
color={PlatformColor('textColor')}
90+
label="textColor"
91+
/>
92+
<ColorSwatch
93+
color={PlatformColor('separatorColor')}
94+
label="separatorColor"
95+
/>
96+
97+
<RNTesterText style={styles.sectionTitle}>
98+
DynamicColorMacOS
2299
</RNTesterText>
100+
<ColorSwatch
101+
color={DynamicColorMacOS({light: '#FF0000', dark: '#00FF00'})}
102+
label="light=red, dark=green"
103+
/>
104+
<ColorSwatch
105+
color={DynamicColorMacOS({light: '#0000FF', dark: '#FFFF00'})}
106+
label="light=blue, dark=yellow"
107+
/>
108+
109+
<RNTesterText style={styles.sectionTitle}>
110+
Background + Text Test
111+
</RNTesterText>
112+
<View
113+
style={[
114+
styles.textBox,
115+
{backgroundColor: PlatformColor('windowBackgroundColor')},
116+
]}>
117+
<RNTesterText style={{color: PlatformColor('labelColor')}}>
118+
This text should be readable in both light and dark mode
119+
</RNTesterText>
120+
</View>
121+
<View
122+
style={[
123+
styles.textBox,
124+
{backgroundColor: PlatformColor('controlBackgroundColor')},
125+
]}>
126+
<RNTesterText style={{color: PlatformColor('controlTextColor')}}>
127+
controlTextColor on controlBackgroundColor
128+
</RNTesterText>
129+
</View>
23130
</View>
24131
);
25132
}
@@ -28,6 +135,43 @@ const styles = StyleSheet.create({
28135
container: {
29136
padding: 10,
30137
},
138+
heading: {
139+
fontSize: 18,
140+
fontWeight: 'bold',
141+
marginBottom: 4,
142+
},
143+
description: {
144+
fontSize: 12,
145+
marginBottom: 12,
146+
color: 'gray',
147+
},
148+
sectionTitle: {
149+
fontSize: 14,
150+
fontWeight: '600',
151+
marginTop: 12,
152+
marginBottom: 6,
153+
},
154+
row: {
155+
flexDirection: 'row',
156+
alignItems: 'center',
157+
marginBottom: 4,
158+
},
159+
swatch: {
160+
width: 40,
161+
height: 24,
162+
borderRadius: 4,
163+
borderWidth: 1,
164+
borderColor: '#ccc',
165+
marginRight: 8,
166+
},
167+
label: {
168+
fontSize: 12,
169+
},
170+
textBox: {
171+
padding: 10,
172+
borderRadius: 6,
173+
marginBottom: 6,
174+
},
31175
});
32176

33177
export default ({

packages/rn-tester/js/utils/testerStateUtils.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ export const Screens = {
2525
} as const;
2626

2727
export const initialNavigationState: RNTesterNavigationState = {
28-
activeModuleKey: null,
29-
activeModuleTitle: null,
28+
activeModuleKey: 'PlaygroundExample',
29+
activeModuleTitle: 'Playground',
3030
activeModuleExampleKey: null,
31-
screen: Screens.COMPONENTS,
31+
screen: Screens.PLAYGROUNDS,
3232
recentlyUsed: {components: [], apis: []},
3333
hadDeepLink: false,
3434
};

0 commit comments

Comments
 (0)