From 7d97100fd63aa40082bbc6d69e8f10eea148b2df Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 21 Mar 2026 17:57:34 -0500 Subject: [PATCH 1/7] 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 #2830 Co-Authored-By: Claude Opus 4.6 --- .../View/RCTViewComponentView.mm | 11 ++ .../renderer/graphics/HostPlatformColor.mm | 44 ++++- .../graphics/RCTPlatformColorUtils.mm | 12 +- .../examples/Playground/RNTesterPlayground.js | 150 +++++++++++++++++- .../rn-tester/js/utils/testerStateUtils.js | 6 +- 5 files changed, 212 insertions(+), 11 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 78b616a03a7..53fb24a0fef 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1177,7 +1177,18 @@ - (void)invalidateLayer #if !TARGET_OS_OSX // [macOS] RCTPlatformColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection]; #else // [macOS + // Resolve dynamic/semantic colors against the current effective appearance + // so that dark mode colors are correctly applied. RCTPlatformColor *backgroundColor = _backgroundColor; + if (_backgroundColor) { + NSAppearance *previousAppearance = NSAppearance.currentAppearance; + NSAppearance.currentAppearance = self.effectiveAppearance ?: [NSApp effectiveAppearance]; + NSColor *resolved = [_backgroundColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + NSAppearance.currentAppearance = previousAppearance; + if (resolved) { + backgroundColor = resolved; + } + } #endif // macOS] // The reason we sometimes do not set self.layer's backgroundColor is because // we want to support non-uniform border radii, which apple does not natively diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index 632e1c7e0db..1ee0b95fb3a 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm @@ -120,7 +120,21 @@ int32_t ColorFromColorComponents(const facebook::react::ColorComponents &compone int32_t ColorFromUIColor(RCTPlatformColor *color) // [macOS] { CGFloat rgba[4]; +#if TARGET_OS_OSX // [macOS + // Resolve dynamic/semantic colors against the current effective appearance + // so that dark mode colors are correctly extracted. + NSAppearance *previousAppearance = NSAppearance.currentAppearance; + NSAppearance.currentAppearance = [NSApp effectiveAppearance]; + NSColor *resolvedColor = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (resolvedColor) { + [resolvedColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + } else { + [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + } + NSAppearance.currentAppearance = previousAppearance; +#else // macOS] [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; +#endif return ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); } @@ -170,9 +184,33 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) return 0; } -#if TARGET_OS_OSX // [macOS] - return ColorFromUIColor(uiColor); -#else // [macOS +#if TARGET_OS_OSX // [macOS + // Hash both light and dark appearance colors to properly distinguish + // dynamic colors that change with appearance. + RCTPlatformColor *color = (RCTPlatformColor *)unwrapManagedObject(uiColor); + int32_t darkColor = 0; + int32_t lightColor = 0; + NSAppearance *previousAppearance = NSAppearance.currentAppearance; + + NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + NSColor *darkResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (darkResolved) { + CGFloat rgba[4]; + [darkResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + darkColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); + } + + NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + NSColor *lightResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (lightResolved) { + CGFloat rgba[4]; + [lightResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + lightColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); + } + + NSAppearance.currentAppearance = previousAppearance; + return facebook::react::hash_combine(darkColor, lightColor); +#else // macOS] static UITraitCollection *darkModeTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]; auto darkColor = ColorFromUIColorForSpecificTraitCollection(uiColor, darkModeTraitCollection); diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm index 7eaac5801fd..e9b2f9e7270 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm @@ -184,9 +184,17 @@ { CGFloat rgba[4]; #if TARGET_OS_OSX // [macOS - color = [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; -#endif // macOS] + // Resolve dynamic/semantic colors against the current effective appearance + // so that dark mode colors are correctly extracted. + NSAppearance *previousAppearance = NSAppearance.currentAppearance; + NSAppearance.currentAppearance = [NSApp effectiveAppearance]; + NSColor *resolvedColor = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + NSAppearance.currentAppearance = previousAppearance; + NSColor *finalColor = resolvedColor ?: [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; + [finalColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; +#else // macOS] [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; +#endif return {(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}; } diff --git a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js index d37d0d4a915..b5165654082 100644 --- a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js +++ b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js @@ -12,14 +12,121 @@ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import RNTesterText from '../../components/RNTesterText'; import * as React from 'react'; -import {StyleSheet, View} from 'react-native'; +import {Platform, PlatformColor, StyleSheet, View} from 'react-native'; +import {DynamicColorMacOS} from 'react-native'; // [macOS] + +function ColorSwatch({ + color, + label, +}: { + color: ReturnType, + label: string, +}) { + return ( + + + {label} + + ); +} function Playground() { + if (Platform.OS !== 'macos') { + return ( + + This test is macOS-only. + + ); + } + return ( - - Edit "RNTesterPlayground.js" to change this file + + Fabric Dark Mode Color Test + + + These colors should change when you toggle between Light and Dark + appearance in System Settings. If they all look the same in both modes, + the bug is not fixed. + + + System Colors + + + + + + Semantic Colors + + + + + + + + + DynamicColorMacOS + + + + + Background + Text Test + + + + This text should be readable in both light and dark mode + + + + + controlTextColor on controlBackgroundColor + + ); } @@ -28,6 +135,43 @@ const styles = StyleSheet.create({ container: { padding: 10, }, + heading: { + fontSize: 18, + fontWeight: 'bold', + marginBottom: 4, + }, + description: { + fontSize: 12, + marginBottom: 12, + color: 'gray', + }, + sectionTitle: { + fontSize: 14, + fontWeight: '600', + marginTop: 12, + marginBottom: 6, + }, + row: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 4, + }, + swatch: { + width: 40, + height: 24, + borderRadius: 4, + borderWidth: 1, + borderColor: '#ccc', + marginRight: 8, + }, + label: { + fontSize: 12, + }, + textBox: { + padding: 10, + borderRadius: 6, + marginBottom: 6, + }, }); export default ({ diff --git a/packages/rn-tester/js/utils/testerStateUtils.js b/packages/rn-tester/js/utils/testerStateUtils.js index 3897338e3ee..1d405e1e98e 100644 --- a/packages/rn-tester/js/utils/testerStateUtils.js +++ b/packages/rn-tester/js/utils/testerStateUtils.js @@ -25,10 +25,10 @@ export const Screens = { } as const; export const initialNavigationState: RNTesterNavigationState = { - activeModuleKey: null, - activeModuleTitle: null, + activeModuleKey: 'PlaygroundExample', + activeModuleTitle: 'Playground', activeModuleExampleKey: null, - screen: Screens.COMPONENTS, + screen: Screens.PLAYGROUNDS, recentlyUsed: {components: [], apis: []}, hadDeepLink: false, }; From 0be4f9418077a47a55bda8d583383a425e326a74 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 21 Mar 2026 19:35:56 -0500 Subject: [PATCH 2/7] Add macOS platform color support to Fabric renderer Add complete macOS semantic color dictionary (label, text, content, control, window colors etc.) and ColorWithSystemEffect support from PR #2737. Includes Swift-style aliases, fallback colors, array-based color selectors, and PlatformColorExample for RNTester. Co-Authored-By: Claude Opus 4.6 --- docsite/api/intro.md | 2 +- docsite/api/platform-color.md | 50 ++++++ docsite/sidebarsApi.ts | 1 + .../renderer/graphics/React-graphics.podspec | 2 +- .../graphics/RCTPlatformColorUtils.mm | 154 +++++++++++++++++- .../PlatformColor/PlatformColorExample.js | 94 +++++++++++ 6 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 docsite/api/platform-color.md diff --git a/docsite/api/intro.md b/docsite/api/intro.md index 06c07fd7618..68454cec6d6 100644 --- a/docsite/api/intro.md +++ b/docsite/api/intro.md @@ -7,4 +7,4 @@ slug: / Welcome to the React Native macOS API reference documentation. This section covers macOS-specific props and events that extend the standard React Native components. -Most of the additional functionality out of React Native macOS directly is in the form of additional props and callback events implemented on ``, to provide macOS and desktop specific behavior +Most of the additional functionality out of React Native macOS directly is in the form of additional props and callback events implemented on ``, to provide macOS and desktop specific behavior. We also have some additional APIs, like platform specific colors. diff --git a/docsite/api/platform-color.md b/docsite/api/platform-color.md new file mode 100644 index 00000000000..486db9a4191 --- /dev/null +++ b/docsite/api/platform-color.md @@ -0,0 +1,50 @@ +--- +sidebar_label: 'Platform Colors' +sidebar_position: 2 +--- + +# Platform Colors + +React Native macOS extends the core `PlatformColor` API with helpers that map directly to AppKit system colors. These helpers make it easier to adopt macOS appearance and accessibility behaviors without writing native code. + +## `DynamicColorMacOS` + +`DynamicColorMacOS` creates a color that automatically adapts to light, dark, and high-contrast appearances on macOS. + +:::note +`DynamicColorIOS` works on macOS too, they are essentially equivalent +::: + +| Option | Description | +| -------------------- | --------------------------------------------------------------- | +| `light` | Color used in the standard light appearance. | +| `dark` | Color used in the standard dark appearance. | +| `highContrastLight` | Optional color for high-contrast light mode. Defaults to `light`.| +| `highContrastDark` | Optional color for high-contrast dark mode. Defaults to `dark`. | + +## `ColorWithSystemEffectMacOS` + +`ColorWithSystemEffectMacOS(color, effect)` wraps an existing color so AppKit can apply control state effects such as pressed, disabled, or rollover. + +| Parameter | Description | +| --------- | ----------- | +| `color` | A string produced by `PlatformColor`, `DynamicColorMacOS`, or a CSS color string. | +| `effect` | One of `none`, `pressed`, `deepPressed`, `disabled`, or `rollover`. | + +```javascript +import { + ColorWithSystemEffectMacOS, + DynamicColorMacOS, + PlatformColor, + StyleSheet, +} from 'react-native'; + +const styles = StyleSheet.create({ + buttonPressed: { + backgroundColor: ColorWithSystemEffectMacOS( + PlatformColor('controlColor'), + 'pressed', + ), + }, +}); +``` diff --git a/docsite/sidebarsApi.ts b/docsite/sidebarsApi.ts index aa2ca6fcd88..df07cc406dd 100644 --- a/docsite/sidebarsApi.ts +++ b/docsite/sidebarsApi.ts @@ -3,6 +3,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; const sidebars: SidebarsConfig = { apiSidebar: [ 'intro', + 'platform-color', 'view-props', 'view-events', ], diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec b/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec index 012b9c0a98d..ed2493b48b8 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec +++ b/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec @@ -41,7 +41,7 @@ Pod::Spec.new do |s| if ENV['USE_FRAMEWORKS'] s.module_name = "React_graphics" s.header_mappings_dir = "../../.." - header_search_paths = header_search_paths + ["\"$(PODS_TARGET_SRCROOT)/platform/ios\""] + header_search_paths = header_search_paths + ["\"$(PODS_TARGET_SRCROOT)/platform/ios\"", "\"$(PODS_TARGET_SRCROOT)/platform/macos\""] # [macOS] end s.pod_target_xcconfig = { "USE_HEADERMAP" => "NO", diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm index e9b2f9e7270..864db1bc41d 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm @@ -8,22 +8,33 @@ #import "RCTPlatformColorUtils.h" #import +#import #import // [macOS] +#if TARGET_OS_OSX // [macOS +#import +#endif // macOS] #import #import #include +#include NS_ASSUME_NONNULL_BEGIN static NSString *const kColorSuffix = @"Color"; static NSString *const kFallbackARGBKey = @"fallback-argb"; +#if TARGET_OS_OSX // [macOS +static NSString *const kFallbackKey = @"fallback"; +static NSString *const kSelectorKey = @"selector"; +static NSString *const kIndexKey = @"index"; +#endif // macOS] static NSDictionary *_PlatformColorSelectorsDict() { static NSDictionary *dict; static dispatch_once_t once_token; dispatch_once(&once_token, ^(void) { +#if !TARGET_OS_OSX // [macOS] dict = @{ // https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors // Label Colors @@ -130,6 +141,105 @@ kFallbackARGBKey : @(0x00000000) // iOS 13.0 }, }; +#else // [macOS + NSMutableDictionary *map = [@{ + // https://developer.apple.com/documentation/appkit/nscolor/ui_element_colors + // Label Colors + @"labelColor": @{}, + @"secondaryLabelColor": @{}, + @"tertiaryLabelColor": @{}, + @"quaternaryLabelColor": @{}, + // Text Colors + @"textColor": @{}, + @"placeholderTextColor": @{}, + @"selectedTextColor": @{}, + @"textBackgroundColor": @{}, + @"selectedTextBackgroundColor": @{}, + @"keyboardFocusIndicatorColor": @{}, + @"unemphasizedSelectedTextColor": @{ + kFallbackKey: @"selectedTextColor" + }, + @"unemphasizedSelectedTextBackgroundColor": @{ + kFallbackKey: @"textBackgroundColor" + }, + // Content Colors + @"linkColor": @{}, + @"separatorColor": @{ + kFallbackKey: @"gridColor" + }, + @"selectedContentBackgroundColor": @{ + kFallbackKey: @"alternateSelectedControlColor" + }, + @"unemphasizedSelectedContentBackgroundColor": @{ + kFallbackKey: @"secondarySelectedControlColor" + }, + // Menu Colors + @"selectedMenuItemTextColor": @{}, + // Table Colors + @"gridColor": @{}, + @"headerTextColor": @{}, + @"alternatingEvenContentBackgroundColor": @{ + kSelectorKey: @"alternatingContentBackgroundColors", + kIndexKey: @0, + kFallbackKey: @"controlAlternatingRowBackgroundColors" + }, + @"alternatingOddContentBackgroundColor": @{ + kSelectorKey: @"alternatingContentBackgroundColors", + kIndexKey: @1, + kFallbackKey: @"controlAlternatingRowBackgroundColors" + }, + // Control Colors + @"controlAccentColor": @{ + kFallbackKey: @"controlColor" + }, + @"controlColor": @{}, + @"controlBackgroundColor": @{}, + @"controlTextColor": @{}, + @"disabledControlTextColor": @{}, + @"selectedControlColor": @{}, + @"selectedControlTextColor": @{}, + @"alternateSelectedControlTextColor": @{}, + @"scrubberTexturedBackgroundColor": @{}, + // Window Colors + @"windowBackgroundColor": @{}, + @"windowFrameTextColor": @{}, + @"underPageBackgroundColor": @{}, + // Highlights and Shadows + @"findHighlightColor": @{ + kFallbackKey: @"highlightColor" + }, + @"highlightColor": @{}, + @"shadowColor": @{}, + // https://developer.apple.com/documentation/appkit/nscolor/standard_colors + // Standard Colors + @"systemBlueColor": @{}, + @"systemBrownColor": @{}, + @"systemGrayColor": @{}, + @"systemGreenColor": @{}, + @"systemOrangeColor": @{}, + @"systemPinkColor": @{}, + @"systemPurpleColor": @{}, + @"systemRedColor": @{}, + @"systemYellowColor": @{}, + // Transparent Color + @"clearColor" : @{}, + } mutableCopy]; + + NSMutableDictionary *aliases = [NSMutableDictionary new]; + for (NSString *objcSelector in map) { + NSMutableDictionary *entry = [map[objcSelector] mutableCopy]; + if ([entry objectForKey:kSelectorKey] == nil) { + entry[kSelectorKey] = objcSelector; + } + if ([objcSelector hasSuffix:kColorSuffix]) { + NSString *swiftSelector = [objcSelector substringToIndex:[objcSelector length] - [kColorSuffix length]]; + aliases[swiftSelector] = entry; + } + } + [map addEntriesFromDictionary:aliases]; + + dict = [map copy]; +#endif // macOS] }); return dict; } @@ -154,21 +264,59 @@ NSDictionary *platformColorSelectorsDict = _PlatformColorSelectorsDict(); NSDictionary *colorInfo = platformColorSelectorsDict[platformColorString]; if (colorInfo) { +#if !TARGET_OS_OSX // [macOS] SEL objcColorSelector = NSSelectorFromString([platformColorString stringByAppendingString:kColorSuffix]); - if (![RCTPlatformColor respondsToSelector:objcColorSelector]) { // [macOS] + if (![RCTPlatformColor respondsToSelector:objcColorSelector]) { NSNumber *fallbackRGB = colorInfo[kFallbackARGBKey]; if (fallbackRGB) { return _UIColorFromHexValue(fallbackRGB); } } else { - Class uiColorClass = [RCTPlatformColor class]; // [macOS] + Class uiColorClass = [RCTPlatformColor class]; IMP imp = [uiColorClass methodForSelector:objcColorSelector]; id (*getUIColor)(id, SEL) = ((id(*)(id, SEL))imp); id colorObject = getUIColor(uiColorClass, objcColorSelector); - if ([colorObject isKindOfClass:[RCTPlatformColor class]]) { // [macOS] + if ([colorObject isKindOfClass:[RCTPlatformColor class]]) { + return colorObject; + } + } +#else // [macOS + NSString *selectorName = colorInfo[kSelectorKey]; + if (selectorName == nil) { + selectorName = [platformColorString stringByAppendingString:kColorSuffix]; + } + + SEL objcColorSelector = NSSelectorFromString(selectorName); + if (![RCTPlatformColor respondsToSelector:objcColorSelector]) { + NSNumber *fallbackRGB = colorInfo[kFallbackARGBKey]; + if (fallbackRGB) { + return _UIColorFromHexValue(fallbackRGB); + } + NSString *fallbackColorName = colorInfo[kFallbackKey]; + if (fallbackColorName) { + return _UIColorFromSemanticString(fallbackColorName); + } + } else { + Class colorClass = [RCTPlatformColor class]; + IMP imp = [colorClass methodForSelector:objcColorSelector]; + id (*getColor)(id, SEL) = ((id(*)(id, SEL))imp); + id colorObject = getColor(colorClass, objcColorSelector); + if ([colorObject isKindOfClass:[NSArray class]]) { + NSNumber *index = colorInfo[kIndexKey]; + if (index != nil) { + NSArray *colors = colorObject; + NSUInteger idx = [index unsignedIntegerValue]; + if (idx < colors.count) { + colorObject = colors[idx]; + } + } + } + + if ([colorObject isKindOfClass:[RCTPlatformColor class]]) { return colorObject; } } +#endif } return nil; } diff --git a/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js b/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js index 5e1e03645d7..eb058fae8c0 100644 --- a/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js +++ b/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js @@ -123,6 +123,100 @@ function PlatformColorsExample() { {label: 'clear', color: PlatformColor('clear')}, {label: 'customColor', color: PlatformColor('customColor')}, ]; + // [macOS + } else if (Platform.OS === 'macos') { + colors = [ + // https://developer.apple.com/documentation/appkit/nscolor + // Text Colors + {label: 'labelColor', color: PlatformColor('labelColor')}, + { + label: 'secondaryLabelColor', + color: PlatformColor('secondaryLabelColor'), + }, + { + label: 'tertiaryLabelColor', + color: PlatformColor('tertiaryLabelColor'), + }, + { + label: 'quaternaryLabelColor', + color: PlatformColor('quaternaryLabelColor'), + }, + {label: 'textColor', color: PlatformColor('textColor')}, + { + label: 'placeholderTextColor', + color: PlatformColor('placeholderTextColor'), + }, + { + label: 'selectedTextColor', + color: PlatformColor('selectedTextColor'), + }, + { + label: 'selectedTextBackgroundColor', + color: PlatformColor('selectedTextBackgroundColor'), + }, + // Window and Control Colors + { + label: 'windowBackgroundColor', + color: PlatformColor('windowBackgroundColor'), + }, + { + label: 'underPageBackgroundColor', + color: PlatformColor('underPageBackgroundColor'), + }, + { + label: 'controlBackgroundColor', + color: PlatformColor('controlBackgroundColor'), + }, + { + label: 'selectedControlColor', + color: PlatformColor('selectedControlColor'), + }, + { + label: 'keyboardFocusIndicatorColor', + color: PlatformColor('keyboardFocusIndicatorColor'), + }, + // System Colors + { + label: 'systemBlueColor', + color: PlatformColor('systemBlueColor'), + }, + { + label: 'systemBrownColor', + color: PlatformColor('systemBrownColor'), + }, + { + label: 'systemGreenColor', + color: PlatformColor('systemGreenColor'), + }, + { + label: 'systemOrangeColor', + color: PlatformColor('systemOrangeColor'), + }, + { + label: 'systemPinkColor', + color: PlatformColor('systemPinkColor'), + }, + { + label: 'systemPurpleColor', + color: PlatformColor('systemPurpleColor'), + }, + { + label: 'systemRedColor', + color: PlatformColor('systemRedColor'), + }, + { + label: 'systemYellowColor', + color: PlatformColor('systemYellowColor'), + }, + // Accents and Grays + { + label: 'controlAccentColor', + color: PlatformColor('controlAccentColor'), + }, + {label: 'separatorColor', color: PlatformColor('separatorColor')}, + {label: 'gridColor', color: PlatformColor('gridColor')}, + {label: 'windowFrameColor', color: PlatformColor('windowFrameColor')}, + ]; // macOS] } else if (Platform.OS === 'android') { colors = [ {label: '?attr/colorAccent', color: PlatformColor('?attr/colorAccent')}, From d19667fce0a81afb2695b428422a158b12012f0a Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 21 Mar 2026 20:10:57 -0500 Subject: [PATCH 3/7] Clean up: fix macOS diff tags and remove test-only changes - Fix bare #endif to use proper #endif // macOS] tags per conventions - Revert Playground and testerStateUtils to upstream defaults (test-only) Co-Authored-By: Claude Opus 4.6 --- .../renderer/graphics/HostPlatformColor.mm | 44 ++++- .../graphics/RCTPlatformColorUtils.mm | 4 +- .../examples/Playground/RNTesterPlayground.js | 150 +----------------- .../rn-tester/js/utils/testerStateUtils.js | 6 +- 4 files changed, 51 insertions(+), 153 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index 1ee0b95fb3a..147fa9617d6 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm @@ -20,6 +20,33 @@ namespace facebook::react { +#if TARGET_OS_OSX // [macOS +RCTUIColor *_Nullable UIColorFromColorWithSystemEffect( + RCTUIColor *baseColor, + const std::string &systemEffectString) +{ + if (baseColor == nil) { + return nil; + } + + NSColor *colorWithEffect = baseColor; + if (!systemEffectString.empty()) { + if (systemEffectString == "none") { + colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectNone]; + } else if (systemEffectString == "pressed") { + colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectPressed]; + } else if (systemEffectString == "deepPressed") { + colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectDeepPressed]; + } else if (systemEffectString == "disabled") { + colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectDisabled]; + } else if (systemEffectString == "rollover") { + colorWithEffect = [baseColor colorWithSystemEffect:NSColorSystemEffectRollover]; + } + } + return colorWithEffect; +} +#endif // macOS] + namespace { bool UIColorIsP3ColorSpace(const std::shared_ptr &uiColor) @@ -134,7 +161,7 @@ int32_t ColorFromUIColor(RCTPlatformColor *color) // [macOS] NSAppearance.currentAppearance = previousAppearance; #else // macOS] [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; -#endif +#endif // macOS] return ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); } @@ -262,6 +289,21 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) 0); } +#if TARGET_OS_OSX // [macOS +Color::Color(const ColorWithSystemEffect &colorWithSystemEffect) +{ + RCTUIColor *baseColor = UIColorFromInt32(colorWithSystemEffect.color); + RCTUIColor *colorWithEffect = + UIColorFromColorWithSystemEffect(baseColor, colorWithSystemEffect.effect); + if (colorWithEffect != nil) { + uiColor_ = wrapManagedObject(colorWithEffect); + } + uiColorHashValue_ = facebook::react::hash_combine( + colorWithSystemEffect.color, + std::hash{}(colorWithSystemEffect.effect)); +} +#endif // macOS + Color::Color(const ColorComponents &components) { uiColor_ = wrapManagedObject(UIColorFromComponentsColor(components)); diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm index 864db1bc41d..12f7a018877 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm @@ -316,7 +316,7 @@ return colorObject; } } -#endif +#endif // macOS] } return nil; } @@ -342,7 +342,7 @@ [finalColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; #else // macOS] [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; -#endif +#endif // macOS] return {(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}; } diff --git a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js index b5165654082..d37d0d4a915 100644 --- a/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js +++ b/packages/rn-tester/js/examples/Playground/RNTesterPlayground.js @@ -12,121 +12,14 @@ import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; import RNTesterText from '../../components/RNTesterText'; import * as React from 'react'; -import {Platform, PlatformColor, StyleSheet, View} from 'react-native'; -import {DynamicColorMacOS} from 'react-native'; // [macOS] - -function ColorSwatch({ - color, - label, -}: { - color: ReturnType, - label: string, -}) { - return ( - - - {label} - - ); -} +import {StyleSheet, View} from 'react-native'; function Playground() { - if (Platform.OS !== 'macos') { - return ( - - This test is macOS-only. - - ); - } - return ( - - Fabric Dark Mode Color Test - - - These colors should change when you toggle between Light and Dark - appearance in System Settings. If they all look the same in both modes, - the bug is not fixed. - - - System Colors - - - - - - Semantic Colors - - - - - - - - - DynamicColorMacOS + + Edit "RNTesterPlayground.js" to change this file - - - - - Background + Text Test - - - - This text should be readable in both light and dark mode - - - - - controlTextColor on controlBackgroundColor - - ); } @@ -135,43 +28,6 @@ const styles = StyleSheet.create({ container: { padding: 10, }, - heading: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 4, - }, - description: { - fontSize: 12, - marginBottom: 12, - color: 'gray', - }, - sectionTitle: { - fontSize: 14, - fontWeight: '600', - marginTop: 12, - marginBottom: 6, - }, - row: { - flexDirection: 'row', - alignItems: 'center', - marginBottom: 4, - }, - swatch: { - width: 40, - height: 24, - borderRadius: 4, - borderWidth: 1, - borderColor: '#ccc', - marginRight: 8, - }, - label: { - fontSize: 12, - }, - textBox: { - padding: 10, - borderRadius: 6, - marginBottom: 6, - }, }); export default ({ diff --git a/packages/rn-tester/js/utils/testerStateUtils.js b/packages/rn-tester/js/utils/testerStateUtils.js index 1d405e1e98e..3897338e3ee 100644 --- a/packages/rn-tester/js/utils/testerStateUtils.js +++ b/packages/rn-tester/js/utils/testerStateUtils.js @@ -25,10 +25,10 @@ export const Screens = { } as const; export const initialNavigationState: RNTesterNavigationState = { - activeModuleKey: 'PlaygroundExample', - activeModuleTitle: 'Playground', + activeModuleKey: null, + activeModuleTitle: null, activeModuleExampleKey: null, - screen: Screens.PLAYGROUNDS, + screen: Screens.COMPONENTS, recentlyUsed: {components: [], apis: []}, hadDeepLink: false, }; From cb52a73360329f54fb749fe105abd78fb17b87b5 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 21 Mar 2026 20:16:25 -0500 Subject: [PATCH 4/7] Remove redundant changes - RCTViewComponentView.mm: RCTUIView.updateLayer already resolves dynamic colors via .CGColor when AppKit calls it with the correct appearance context - PlatformColorExample.js: macOS platform color examples already exist in Paper renderer Co-Authored-By: Claude Opus 4.6 --- .../View/RCTViewComponentView.mm | 11 --- .../PlatformColor/PlatformColorExample.js | 94 ------------------- 2 files changed, 105 deletions(-) diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index 53fb24a0fef..78b616a03a7 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -1177,18 +1177,7 @@ - (void)invalidateLayer #if !TARGET_OS_OSX // [macOS] RCTPlatformColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection]; #else // [macOS - // Resolve dynamic/semantic colors against the current effective appearance - // so that dark mode colors are correctly applied. RCTPlatformColor *backgroundColor = _backgroundColor; - if (_backgroundColor) { - NSAppearance *previousAppearance = NSAppearance.currentAppearance; - NSAppearance.currentAppearance = self.effectiveAppearance ?: [NSApp effectiveAppearance]; - NSColor *resolved = [_backgroundColor colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; - NSAppearance.currentAppearance = previousAppearance; - if (resolved) { - backgroundColor = resolved; - } - } #endif // macOS] // The reason we sometimes do not set self.layer's backgroundColor is because // we want to support non-uniform border radii, which apple does not natively diff --git a/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js b/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js index eb058fae8c0..5e1e03645d7 100644 --- a/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js +++ b/packages/rn-tester/js/examples/PlatformColor/PlatformColorExample.js @@ -123,100 +123,6 @@ function PlatformColorsExample() { {label: 'clear', color: PlatformColor('clear')}, {label: 'customColor', color: PlatformColor('customColor')}, ]; - // [macOS - } else if (Platform.OS === 'macos') { - colors = [ - // https://developer.apple.com/documentation/appkit/nscolor - // Text Colors - {label: 'labelColor', color: PlatformColor('labelColor')}, - { - label: 'secondaryLabelColor', - color: PlatformColor('secondaryLabelColor'), - }, - { - label: 'tertiaryLabelColor', - color: PlatformColor('tertiaryLabelColor'), - }, - { - label: 'quaternaryLabelColor', - color: PlatformColor('quaternaryLabelColor'), - }, - {label: 'textColor', color: PlatformColor('textColor')}, - { - label: 'placeholderTextColor', - color: PlatformColor('placeholderTextColor'), - }, - { - label: 'selectedTextColor', - color: PlatformColor('selectedTextColor'), - }, - { - label: 'selectedTextBackgroundColor', - color: PlatformColor('selectedTextBackgroundColor'), - }, - // Window and Control Colors - { - label: 'windowBackgroundColor', - color: PlatformColor('windowBackgroundColor'), - }, - { - label: 'underPageBackgroundColor', - color: PlatformColor('underPageBackgroundColor'), - }, - { - label: 'controlBackgroundColor', - color: PlatformColor('controlBackgroundColor'), - }, - { - label: 'selectedControlColor', - color: PlatformColor('selectedControlColor'), - }, - { - label: 'keyboardFocusIndicatorColor', - color: PlatformColor('keyboardFocusIndicatorColor'), - }, - // System Colors - { - label: 'systemBlueColor', - color: PlatformColor('systemBlueColor'), - }, - { - label: 'systemBrownColor', - color: PlatformColor('systemBrownColor'), - }, - { - label: 'systemGreenColor', - color: PlatformColor('systemGreenColor'), - }, - { - label: 'systemOrangeColor', - color: PlatformColor('systemOrangeColor'), - }, - { - label: 'systemPinkColor', - color: PlatformColor('systemPinkColor'), - }, - { - label: 'systemPurpleColor', - color: PlatformColor('systemPurpleColor'), - }, - { - label: 'systemRedColor', - color: PlatformColor('systemRedColor'), - }, - { - label: 'systemYellowColor', - color: PlatformColor('systemYellowColor'), - }, - // Accents and Grays - { - label: 'controlAccentColor', - color: PlatformColor('controlAccentColor'), - }, - {label: 'separatorColor', color: PlatformColor('separatorColor')}, - {label: 'gridColor', color: PlatformColor('gridColor')}, - {label: 'windowFrameColor', color: PlatformColor('windowFrameColor')}, - ]; // macOS] } else if (Platform.OS === 'android') { colors = [ {label: '?attr/colorAccent', color: PlatformColor('?attr/colorAccent')}, From 1f70ceb178c186bb1d88caae119d5caf4f72f667 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 21 Mar 2026 20:58:56 -0500 Subject: [PATCH 5/7] Address PR feedback: fix macOS tags and make all ifdef blocks iOS-first - Fix #else/#endif comment tags to follow diffs-with-upstream guide - Flip all macOS-first (#if TARGET_OS_OSX) blocks with #else to iOS-first - Remove unnecessary TargetConditionals.h and AppKit.h imports - Add back missing [macOS] tags on modified lines Co-Authored-By: Claude Opus 4.6 --- .../renderer/graphics/HostPlatformColor.h | 10 +++ .../renderer/graphics/HostPlatformColor.mm | 62 +++++++++---------- .../renderer/graphics/PlatformColorParser.mm | 18 ++++++ .../graphics/RCTPlatformColorUtils.mm | 18 +++--- 4 files changed, 66 insertions(+), 42 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h index 8deffedcb1d..bd8720e6670 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.h @@ -20,9 +20,19 @@ struct DynamicColor { int32_t highContrastDarkColor = 0; }; +#if TARGET_OS_OSX // [macOS +struct ColorWithSystemEffect { + int32_t color = 0; + std::string effect; +}; +#endif // macOS] + struct Color { Color(int32_t color); Color(const DynamicColor& dynamicColor); +#if TARGET_OS_OSX // [macOS + Color(const ColorWithSystemEffect& colorWithSystemEffect); +#endif // macOS] Color(const ColorComponents& components); Color() : uiColor_(nullptr){}; int32_t getColor() const; diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index 147fa9617d6..e98c9afaa0a 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm @@ -147,7 +147,9 @@ int32_t ColorFromColorComponents(const facebook::react::ColorComponents &compone int32_t ColorFromUIColor(RCTPlatformColor *color) // [macOS] { CGFloat rgba[4]; -#if TARGET_OS_OSX // [macOS +#if !TARGET_OS_OSX // [macOS] + [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; +#else // [macOS // Resolve dynamic/semantic colors against the current effective appearance // so that dark mode colors are correctly extracted. NSAppearance *previousAppearance = NSAppearance.currentAppearance; @@ -159,8 +161,6 @@ int32_t ColorFromUIColor(RCTPlatformColor *color) // [macOS] [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; } NSAppearance.currentAppearance = previousAppearance; -#else // macOS] - [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; #endif // macOS] return ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); } @@ -211,33 +211,7 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) return 0; } -#if TARGET_OS_OSX // [macOS - // Hash both light and dark appearance colors to properly distinguish - // dynamic colors that change with appearance. - RCTPlatformColor *color = (RCTPlatformColor *)unwrapManagedObject(uiColor); - int32_t darkColor = 0; - int32_t lightColor = 0; - NSAppearance *previousAppearance = NSAppearance.currentAppearance; - - NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; - NSColor *darkResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; - if (darkResolved) { - CGFloat rgba[4]; - [darkResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; - darkColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); - } - - NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; - NSColor *lightResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; - if (lightResolved) { - CGFloat rgba[4]; - [lightResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; - lightColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); - } - - NSAppearance.currentAppearance = previousAppearance; - return facebook::react::hash_combine(darkColor, lightColor); -#else // macOS] +#if !TARGET_OS_OSX // [macOS] static UITraitCollection *darkModeTraitCollection = [UITraitCollection traitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]; auto darkColor = ColorFromUIColorForSpecificTraitCollection(uiColor, darkModeTraitCollection); @@ -267,6 +241,32 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) darkAccessibilityContrastColor, lightAccessibilityContrastColor, UIColorIsP3ColorSpace(uiColor)); +#else // [macOS + // Hash both light and dark appearance colors to properly distinguish + // dynamic colors that change with appearance. + RCTPlatformColor *color = (RCTPlatformColor *)unwrapManagedObject(uiColor); + int32_t darkColor = 0; + int32_t lightColor = 0; + NSAppearance *previousAppearance = NSAppearance.currentAppearance; + + NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; + NSColor *darkResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (darkResolved) { + CGFloat rgba[4]; + [darkResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + darkColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); + } + + NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; + NSColor *lightResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (lightResolved) { + CGFloat rgba[4]; + [lightResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + lightColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); + } + + NSAppearance.currentAppearance = previousAppearance; + return facebook::react::hash_combine(darkColor, lightColor); #endif // macOS] } @@ -302,7 +302,7 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) colorWithSystemEffect.color, std::hash{}(colorWithSystemEffect.effect)); } -#endif // macOS +#endif // macOS] Color::Color(const ColorComponents &components) { diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm index 7cb04798066..859bc9e291e 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/PlatformColorParser.mm @@ -62,6 +62,24 @@ SharedColor parsePlatformColor(const ContextContainer &contextContainer, int32_t items.at("dynamic").hasType>()) { auto dynamicItems = (std::unordered_map)items.at("dynamic"); return RCTPlatformColorComponentsFromDynamicItems(contextContainer, surfaceId, dynamicItems); +#if TARGET_OS_OSX // [macOS + } else if ( + items.find("colorWithSystemEffect") != items.end() && + items.at("colorWithSystemEffect").hasType>()) { + auto colorWithSystemEffectItems = + (std::unordered_map)items.at("colorWithSystemEffect"); + if (colorWithSystemEffectItems.find("baseColor") != colorWithSystemEffectItems.end() && + colorWithSystemEffectItems.find("systemEffect") != colorWithSystemEffectItems.end() && + colorWithSystemEffectItems.at("systemEffect").hasType()) { + SharedColor baseColorShared{}; + fromRawValue(contextContainer, surfaceId, colorWithSystemEffectItems.at("baseColor"), baseColorShared); + if (baseColorShared) { + std::string systemEffect = (std::string)colorWithSystemEffectItems.at("systemEffect"); + auto baseColor = (*baseColorShared).getColor(); + return SharedColor(Color(ColorWithSystemEffect{baseColor, systemEffect})); + } + } +#endif // macOS] } } diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm index 12f7a018877..b9b76daa27f 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/RCTPlatformColorUtils.mm @@ -8,16 +8,12 @@ #import "RCTPlatformColorUtils.h" #import -#import #import // [macOS] -#if TARGET_OS_OSX // [macOS -#import -#endif // macOS] #import #import #include -#include +#include // [macOS] NS_ASSUME_NONNULL_BEGIN @@ -266,17 +262,17 @@ if (colorInfo) { #if !TARGET_OS_OSX // [macOS] SEL objcColorSelector = NSSelectorFromString([platformColorString stringByAppendingString:kColorSuffix]); - if (![RCTPlatformColor respondsToSelector:objcColorSelector]) { + if (![RCTPlatformColor respondsToSelector:objcColorSelector]) { // [macOS] NSNumber *fallbackRGB = colorInfo[kFallbackARGBKey]; if (fallbackRGB) { return _UIColorFromHexValue(fallbackRGB); } } else { - Class uiColorClass = [RCTPlatformColor class]; + Class uiColorClass = [RCTPlatformColor class]; // [macOS] IMP imp = [uiColorClass methodForSelector:objcColorSelector]; id (*getUIColor)(id, SEL) = ((id(*)(id, SEL))imp); id colorObject = getUIColor(uiColorClass, objcColorSelector); - if ([colorObject isKindOfClass:[RCTPlatformColor class]]) { + if ([colorObject isKindOfClass:[RCTPlatformColor class]]) { // [macOS] return colorObject; } } @@ -331,7 +327,9 @@ static inline facebook::react::ColorComponents _ColorComponentsFromUIColor(RCTPlatformColor *color) // [macOS] { CGFloat rgba[4]; -#if TARGET_OS_OSX // [macOS +#if !TARGET_OS_OSX // [macOS] + [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; +#else // [macOS // Resolve dynamic/semantic colors against the current effective appearance // so that dark mode colors are correctly extracted. NSAppearance *previousAppearance = NSAppearance.currentAppearance; @@ -340,8 +338,6 @@ NSAppearance.currentAppearance = previousAppearance; NSColor *finalColor = resolvedColor ?: [color colorUsingColorSpace:[NSColorSpace genericRGBColorSpace]]; [finalColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; -#else // macOS] - [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; #endif // macOS] return {(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}; } From 68dbc72837e64f4ee6d2ed779eda2614f513a7dc Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Sat, 21 Mar 2026 21:03:28 -0500 Subject: [PATCH 6/7] Revert unnecessary podspec header search path change No platform/macos directory exists since all code is inline in platform/ios with #if TARGET_OS_OSX guards. Co-Authored-By: Claude Opus 4.6 --- .../ReactCommon/react/renderer/graphics/React-graphics.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec b/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec index ed2493b48b8..012b9c0a98d 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec +++ b/packages/react-native/ReactCommon/react/renderer/graphics/React-graphics.podspec @@ -41,7 +41,7 @@ Pod::Spec.new do |s| if ENV['USE_FRAMEWORKS'] s.module_name = "React_graphics" s.header_mappings_dir = "../../.." - header_search_paths = header_search_paths + ["\"$(PODS_TARGET_SRCROOT)/platform/ios\"", "\"$(PODS_TARGET_SRCROOT)/platform/macos\""] # [macOS] + header_search_paths = header_search_paths + ["\"$(PODS_TARGET_SRCROOT)/platform/ios\""] end s.pod_target_xcconfig = { "USE_HEADERMAP" => "NO", From f1400a06af9c41bf8883cb172f7ca33c57310403 Mon Sep 17 00:00:00 2001 From: Saad Najmi Date: Mon, 23 Mar 2026 10:59:17 -0500 Subject: [PATCH 7/7] Use performAsCurrentDrawingAppearance for appearance resolution Replace manual NSAppearance save/restore with the cleaner performAsCurrentDrawingAppearance API (available since macOS 11.0, our min target is 14.0). This is exception-safe and more readable. Co-Authored-By: Claude Opus 4.6 --- .../renderer/graphics/HostPlatformColor.mm | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm index e98c9afaa0a..290b18e1128 100644 --- a/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm +++ b/packages/react-native/ReactCommon/react/renderer/graphics/platform/ios/react/renderer/graphics/HostPlatformColor.mm @@ -152,15 +152,14 @@ int32_t ColorFromUIColor(RCTPlatformColor *color) // [macOS] #else // [macOS // Resolve dynamic/semantic colors against the current effective appearance // so that dark mode colors are correctly extracted. - NSAppearance *previousAppearance = NSAppearance.currentAppearance; - NSAppearance.currentAppearance = [NSApp effectiveAppearance]; - NSColor *resolvedColor = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; - if (resolvedColor) { - [resolvedColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; - } else { - [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; - } - NSAppearance.currentAppearance = previousAppearance; + [[NSApp effectiveAppearance] performAsCurrentDrawingAppearance:^{ + NSColor *resolvedColor = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (resolvedColor) { + [resolvedColor getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + } else { + [color getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + } + }]; #endif // macOS] return ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); } @@ -245,27 +244,27 @@ int32_t ColorFromUIColor(const std::shared_ptr &uiColor) // Hash both light and dark appearance colors to properly distinguish // dynamic colors that change with appearance. RCTPlatformColor *color = (RCTPlatformColor *)unwrapManagedObject(uiColor); - int32_t darkColor = 0; - int32_t lightColor = 0; - NSAppearance *previousAppearance = NSAppearance.currentAppearance; - - NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; - NSColor *darkResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; - if (darkResolved) { - CGFloat rgba[4]; - [darkResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; - darkColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); - } - - NSAppearance.currentAppearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; - NSColor *lightResolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; - if (lightResolved) { - CGFloat rgba[4]; - [lightResolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; - lightColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); - } + __block int32_t darkColor = 0; + __block int32_t lightColor = 0; + + [[NSAppearance appearanceNamed:NSAppearanceNameDarkAqua] performAsCurrentDrawingAppearance:^{ + NSColor *resolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (resolved) { + CGFloat rgba[4]; + [resolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + darkColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); + } + }]; + + [[NSAppearance appearanceNamed:NSAppearanceNameAqua] performAsCurrentDrawingAppearance:^{ + NSColor *resolved = [color colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + if (resolved) { + CGFloat rgba[4]; + [resolved getRed:&rgba[0] green:&rgba[1] blue:&rgba[2] alpha:&rgba[3]]; + lightColor = ColorFromColorComponents({(float)rgba[0], (float)rgba[1], (float)rgba[2], (float)rgba[3]}); + } + }]; - NSAppearance.currentAppearance = previousAppearance; return facebook::react::hash_combine(darkColor, lightColor); #endif // macOS] }