From 279f29589978e6ef40979ee8aa27786be624a5eb Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 6 May 2026 03:09:11 -0700 Subject: [PATCH 1/3] Remove 'unspecified' from ColorSchemeName (#56686) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Removes `'unspecified'` from the return type of `Appearance.getColorScheme()` and `useColorScheme()`, splitting the setter input into a separate `ColorSchemeOverride` type. This resolves a longstanding misalignment between what native returns and what the types promise. **Motivation** `'unspecified'` is only meaningful as an input to `setColorScheme()` — neither iOS nor Android ever returns it from `getColorScheme()`. When `setColorScheme('unspecified')` is called, the JS layer re-queries the native module and caches the resolved system value. After this change: - `Appearance.getColorScheme()` returns `'light' | 'dark'` (no longer `'unspecified'`) - `Appearance.setColorScheme()` receives `'light' | 'dark' | 'unspecified'` Paired with docs updates: - https://github.com/facebook/react-native-website/pull/5060 - https://github.com/facebook/react-native-website/pull/5069 **History of this API** - The TurboModule spec originally typed these methods as plain `string` because codegen didn't support union types (T52919652). - When support landed, D63681874 upgraded to `ColorSchemeName = 'light' | 'dark' | 'unspecified'` — a type-level cleanup that inadvertently widened return types to include `'unspecified'`, a value native never returns. This caused `$FlowFixMe` suppressions across downstream callers. - D80705652 later aligned the `.d.ts` and fixed a bug where `setColorScheme('unspecified')` threw an incorrect invariant. Changelog: [General][Breaking] - `useColorScheme()` no longer returns `'unspecified'` (this was always the case, but is a breaking type change) Reviewed By: cipolleschi Differential Revision: D102527387 --- .../Libraries/Utilities/Appearance.d.ts | 35 +++++++++++-------- .../Libraries/Utilities/Appearance.js | 35 +++++++++++++------ .../Libraries/Utilities/useColorScheme.js | 8 +++++ packages/react-native/ReactNativeApi.d.ts | 15 ++++---- .../modules/NativeAppearance.js | 6 ++-- .../examples/Appearance/AppearanceExample.js | 7 ++-- 6 files changed, 70 insertions(+), 36 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index fe02b80b972d..a271aa02caeb 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -9,7 +9,9 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter'; -type ColorSchemeName = 'light' | 'dark' | 'unspecified'; +type ColorSchemeName = 'light' | 'dark'; + +type ColorSchemeOverride = ColorSchemeName | 'unspecified'; export namespace Appearance { type AppearancePreferences = { @@ -19,25 +21,30 @@ export namespace Appearance { type AppearanceListener = (preferences: AppearancePreferences) => void; /** - * Note: Although color scheme is available immediately, it may change at any - * time. Any rendering logic or styles that depend on this should try to call - * this function on every render, rather than caching the value (for example, - * using inline styles rather than setting a value in a `StyleSheet`). + * Returns the active color scheme (`'light'` or `'dark'`). This value may + * change at runtime, either at the system level (e.g. scheduled color scheme + * change at sunrise or sunset) or when overridden at the app level via + * `setColorScheme()`. + * + * Prefer `useColorScheme()` in React components. * - * Example: `const colorScheme = Appearance.getColorScheme();` + * Notes: + * - `null` will only be returned if the native Appearance module is + * unavailable (out of tree platforms). */ export function getColorScheme(): ColorSchemeName | null | undefined; /** - * Set the color scheme preference. This is useful for overriding the default - * color scheme preference for the app. Note that this will not change the - * appearance of the system UI, only the appearance of the app. - * Only available on iOS 13+ and Android 10+. + * Force the application to always adopt a light or dark interface style. Pass + * `'unspecified'` to reset and follow the system default (removes any + * override). This does not affect the system UI, only the application. */ - export function setColorScheme(scheme: ColorSchemeName): void; + export function setColorScheme(scheme: ColorSchemeOverride): void; /** - * Add an event handler that is fired when appearance preferences change. + * Subscribe to color scheme changes. The listener receives the new appearance + * preferences whenever the color scheme changes, whether from a system event + * or a call to `setColorScheme()`. */ export function addChangeListener( listener: AppearanceListener, @@ -45,7 +52,7 @@ export namespace Appearance { } /** - * A new useColorScheme hook is provided as the preferred way of accessing - * the user's preferred color scheme (e.g. Dark Mode). + * Returns the active color scheme (`'light'` or `'dark'`). Automatically + * re-renders the component when the color scheme changes. */ export function useColorScheme(): ColorSchemeName; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 8b2bdba03c83..05b32bb2dbb6 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -9,13 +9,17 @@ */ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; -import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance'; +import type { + AppearancePreferences, + ColorSchemeName, + ColorSchemeOverride, +} from './NativeAppearance'; import typeof INativeAppearance from './NativeAppearance'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import EventEmitter from '../vendor/emitter/EventEmitter'; -export type {AppearancePreferences}; +export type {AppearancePreferences, ColorSchemeName, ColorSchemeOverride}; type Appearance = { colorScheme: ?ColorSchemeName, @@ -69,9 +73,16 @@ function getState(): NonNullable { } /** - * Returns the current color scheme preference. This value may change, so the - * value should not be cached without either listening to changes or using - * the `useColorScheme` hook. + * Returns the active color scheme (`'light'` or `'dark'`). This value may + * change at runtime, either at the system level (e.g. scheduled color scheme + * change at sunrise or sunset) or when overridden at the app level via + * `setColorScheme()`. + * + * Prefer `useColorScheme()` in React components. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). */ export function getColorScheme(): ?ColorSchemeName { let colorScheme = null; @@ -91,26 +102,28 @@ export function getColorScheme(): ?ColorSchemeName { } /** - * Updates the current color scheme to the supplied value. + * Force the application to always adopt a light or dark interface style. Pass + * `'unspecified'` to reset and follow the system default (removes any + * override). This does not affect the system UI, only the application. */ -export function setColorScheme(colorScheme: ColorSchemeName): void { +export function setColorScheme(colorScheme: ColorSchemeOverride): void { const state = getState(); const {NativeAppearance} = state; if (NativeAppearance != null) { NativeAppearance.setColorScheme(colorScheme); state.appearance = { - // When setting to 'unspecified', get the actual system color scheme. - // Fall back to the passed value if getColorScheme() returns null. colorScheme: colorScheme === 'unspecified' - ? (NativeAppearance.getColorScheme() ?? colorScheme) + ? (NativeAppearance.getColorScheme() ?? null) : colorScheme, }; } } /** - * Add an event handler that is fired when appearance preferences change. + * Subscribe to color scheme changes. The listener receives the new appearance + * preferences whenever the color scheme changes, whether from a system event + * or a call to `setColorScheme()`. */ export function addChangeListener( listener: ({colorScheme: ?ColorSchemeName}) => void, diff --git a/packages/react-native/Libraries/Utilities/useColorScheme.js b/packages/react-native/Libraries/Utilities/useColorScheme.js index ab5f6de5256f..45eb00fa8cac 100644 --- a/packages/react-native/Libraries/Utilities/useColorScheme.js +++ b/packages/react-native/Libraries/Utilities/useColorScheme.js @@ -20,6 +20,14 @@ const subscribe = (onStoreChange: () => void) => { return () => appearanceSubscription.remove(); }; +/** + * Returns the active color scheme (`'light'` or `'dark'`). Automatically + * re-renders the component when the color scheme changes. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). + */ export default function useColorScheme(): ?ColorSchemeName { return useSyncExternalStore(subscribe, getColorScheme); } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index 13c53f3f59ab..c3f957f6688a 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<1c8637ab03a5fec9d39704d1ae305595>> + * @generated SignedSource<<4950f1efd16fed02b526f83325c8351d>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1630,6 +1630,8 @@ declare namespace Appearance { setColorScheme, addChangeListener, AppearancePreferences, + ColorSchemeName, + ColorSchemeOverride, } } declare type AppearancePreferences = { @@ -1856,7 +1858,8 @@ declare namespace CodegenTypes { } } declare type ColorListenerCallback = (value: ColorValue) => unknown -declare type ColorSchemeName = "dark" | "light" | "unspecified" +declare type ColorSchemeName = "dark" | "light" +declare type ColorSchemeOverride = "dark" | "light" | "unspecified" declare type ColorValue = ____ColorValue_Internal declare type ComponentProvider = () => React.ComponentType declare type ComponentProviderInstrumentationHook = ( @@ -4709,7 +4712,7 @@ declare type Separators = { updateProps: (select: "leading" | "trailing", newProps: Object) => void } declare type sequence = typeof sequence -declare function setColorScheme(colorScheme: ColorSchemeName): void +declare function setColorScheme(colorScheme: ColorSchemeOverride): void declare function setComponentProviderInstrumentationHook( hook: ComponentProviderInstrumentationHook, ): void @@ -6070,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // 00cbaa0a + Appearance, // df9545f9 AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 @@ -6080,7 +6083,7 @@ export { ButtonProps, // 0df9cb59 Clipboard, // 41addb89 CodegenTypes, // 0b8108a8 - ColorSchemeName, // 31a4350e + ColorSchemeName, // 6615edd6 ColorValue, // 98989a8f ComponentProvider, // b5c60ddd ComponentProviderInstrumentationHook, // 9f640048 @@ -6336,7 +6339,7 @@ export { useAnimatedColor, // e3511f81 useAnimatedValue, // b18adb63 useAnimatedValueXY, // c7ee2332 - useColorScheme, // c216d6f7 + useColorScheme, // 29a517d5 usePressability, // b4e21b46 useWindowDimensions, // bb4b683f } diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js index 29296fe87921..dca2c6f8a08f 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -12,7 +12,9 @@ import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport'; import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry'; -export type ColorSchemeName = 'light' | 'dark' | 'unspecified'; +export type ColorSchemeName = 'light' | 'dark'; + +export type ColorSchemeOverride = 'light' | 'dark' | 'unspecified'; export type AppearancePreferences = { colorScheme?: ?ColorSchemeName, @@ -20,7 +22,7 @@ export type AppearancePreferences = { export interface Spec extends TurboModule { +getColorScheme: () => ?ColorSchemeName; - +setColorScheme: (colorScheme: ColorSchemeName) => void; + +setColorScheme: (colorScheme: ColorSchemeOverride) => void; // RCTEventEmitter +addListener: (eventName: string) => void; diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 7285806793a8..7fd44d84f22b 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -18,7 +18,7 @@ import {useEffect, useState} from 'react'; import {Appearance, Button, Text, View, useColorScheme} from 'react-native'; function ColorSchemeSubscription() { - const [colorScheme, setColorScheme] = useState( + const [colorScheme, setColorScheme] = useState( Appearance.getColorScheme(), ); @@ -135,8 +135,9 @@ const ColorShowcase = (props: {themeName: string}) => ( ); const ToggleNativeAppearance = () => { - const [nativeColorScheme, setNativeColorScheme] = - useState('unspecified'); + const [nativeColorScheme, setNativeColorScheme] = useState< + ColorSchemeName | 'unspecified', + >('unspecified'); const colorScheme = useColorScheme(); useEffect(() => { From 42c3b430864cdd681a03161f551e3deeb6b3737b Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 6 May 2026 03:09:11 -0700 Subject: [PATCH 2/3] Align nullable types across Appearance API (#56687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Simplify and align nullable values in the `Appearance` API. | API | Flow / Strict TS API | Manual `.d.ts` | |---|---|---| | `getColorScheme()` | `ColorSchemeName | null` (narrowed) | `ColorSchemeName | null` | | `useColorScheme()` | `ColorSchemeName | null` (narrowed) | `ColorSchemeName | null` (fixed) | At the native spec level, nullability is removed entirely — the module always returns a valid color scheme, and the module-level `?Spec` already covers the "module absent" case. Changelog: [Internal] - This is a net fix/extension of D102527387 Differential Revision: D103865622 --- .../Libraries/Utilities/Appearance.d.ts | 10 +++++++--- .../Libraries/Utilities/Appearance.js | 18 +++++++++++------- .../Libraries/Utilities/useColorScheme.js | 2 +- packages/react-native/ReactNativeApi.d.ts | 16 ++++++++-------- .../modules/NativeAppearance.js | 4 ++-- .../examples/Appearance/AppearanceExample.js | 2 +- .../api-snapshots/ReactAppleDebugCxx.api | 2 +- .../api-snapshots/ReactAppleReleaseCxx.api | 2 +- 8 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index a271aa02caeb..c37a0ea577d2 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -15,7 +15,7 @@ type ColorSchemeOverride = ColorSchemeName | 'unspecified'; export namespace Appearance { type AppearancePreferences = { - colorScheme: ColorSchemeName; + colorScheme: ColorSchemeName | null; }; type AppearanceListener = (preferences: AppearancePreferences) => void; @@ -32,7 +32,7 @@ export namespace Appearance { * - `null` will only be returned if the native Appearance module is * unavailable (out of tree platforms). */ - export function getColorScheme(): ColorSchemeName | null | undefined; + export function getColorScheme(): ColorSchemeName | null; /** * Force the application to always adopt a light or dark interface style. Pass @@ -54,5 +54,9 @@ export namespace Appearance { /** * Returns the active color scheme (`'light'` or `'dark'`). Automatically * re-renders the component when the color scheme changes. + * + * Notes: + * - `null` will only be returned if the native Appearance module is unavailable + * (out of tree platforms). */ -export function useColorScheme(): ColorSchemeName; +export function useColorScheme(): ColorSchemeName | null; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 05b32bb2dbb6..4af8fe2e6b36 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -10,7 +10,7 @@ import type {EventSubscription} from '../vendor/emitter/EventEmitter'; import type { - AppearancePreferences, + AppearancePreferences as NativeAppearancePreferences, ColorSchemeName, ColorSchemeOverride, } from './NativeAppearance'; @@ -19,10 +19,14 @@ import typeof INativeAppearance from './NativeAppearance'; import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; import EventEmitter from '../vendor/emitter/EventEmitter'; -export type {AppearancePreferences, ColorSchemeName, ColorSchemeOverride}; +export type {ColorSchemeName, ColorSchemeOverride}; + +export type AppearancePreferences = { + colorScheme: ColorSchemeName | null, +}; type Appearance = { - colorScheme: ?ColorSchemeName, + colorScheme: ColorSchemeName | null, }; let lazyState: ?{ @@ -60,7 +64,7 @@ function getState(): NonNullable { eventEmitter, }; new NativeEventEmitter<{ - appearanceChanged: [AppearancePreferences], + appearanceChanged: [NativeAppearancePreferences], }>(NativeAppearance).addListener('appearanceChanged', newAppearance => { state.appearance = { colorScheme: newAppearance.colorScheme, @@ -84,7 +88,7 @@ function getState(): NonNullable { * - `null` will only be returned if the native Appearance module is unavailable * (out of tree platforms). */ -export function getColorScheme(): ?ColorSchemeName { +export function getColorScheme(): ColorSchemeName | null { let colorScheme = null; const state = getState(); const {NativeAppearance} = state; @@ -114,7 +118,7 @@ export function setColorScheme(colorScheme: ColorSchemeOverride): void { state.appearance = { colorScheme: colorScheme === 'unspecified' - ? (NativeAppearance.getColorScheme() ?? null) + ? NativeAppearance.getColorScheme() : colorScheme, }; } @@ -126,7 +130,7 @@ export function setColorScheme(colorScheme: ColorSchemeOverride): void { * or a call to `setColorScheme()`. */ export function addChangeListener( - listener: ({colorScheme: ?ColorSchemeName}) => void, + listener: (preferences: AppearancePreferences) => void, ): EventSubscription { const {eventEmitter} = getState(); return eventEmitter.addListener('change', listener); diff --git a/packages/react-native/Libraries/Utilities/useColorScheme.js b/packages/react-native/Libraries/Utilities/useColorScheme.js index 45eb00fa8cac..39cec85a3031 100644 --- a/packages/react-native/Libraries/Utilities/useColorScheme.js +++ b/packages/react-native/Libraries/Utilities/useColorScheme.js @@ -28,6 +28,6 @@ const subscribe = (onStoreChange: () => void) => { * - `null` will only be returned if the native Appearance module is unavailable * (out of tree platforms). */ -export default function useColorScheme(): ?ColorSchemeName { +export default function useColorScheme(): ColorSchemeName | null { return useSyncExternalStore(subscribe, getColorScheme); } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index c3f957f6688a..a2538b661fd0 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<4950f1efd16fed02b526f83325c8351d>> + * @generated SignedSource<<04955f1605996352fa3f3a3fc86ba937>> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1218,7 +1218,7 @@ declare type ActivityIndicatorProps = Readonly< > declare type add = typeof add declare function addChangeListener( - listener: ($$PARAM_0$$: { colorScheme: ColorSchemeName | undefined }) => void, + listener: (preferences: AppearancePreferences) => void, ): EventSubscription declare class Alert { static alert( @@ -1629,13 +1629,13 @@ declare namespace Appearance { getColorScheme, setColorScheme, addChangeListener, - AppearancePreferences, ColorSchemeName, ColorSchemeOverride, + AppearancePreferences, } } declare type AppearancePreferences = { - colorScheme?: ColorSchemeName + colorScheme: ColorSchemeName | null } declare type AppParameters = { initialProps: { @@ -2468,7 +2468,7 @@ declare function get_2( name: string, ): null | T | undefined declare function getAppKeys(): ReadonlyArray -declare function getColorScheme(): ColorSchemeName | null | undefined +declare function getColorScheme(): ColorSchemeName | null declare function getEnforcing(name: string): T declare function getRegistry(): Registry declare function getRunnable(appKey: string): null | Runnable | undefined @@ -5738,7 +5738,7 @@ declare function useAnimatedValueXY( }, config?: Animated.AnimatedConfig | null | undefined, ): Animated.ValueXY -declare function useColorScheme(): ColorSchemeName | null | undefined +declare function useColorScheme(): ColorSchemeName | null declare function usePressability( config: null | PressabilityConfig | undefined, ): null | PressabilityEventHandlers @@ -6073,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // df9545f9 + Appearance, // 0e9bf4fe AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 @@ -6339,7 +6339,7 @@ export { useAnimatedColor, // e3511f81 useAnimatedValue, // b18adb63 useAnimatedValueXY, // c7ee2332 - useColorScheme, // 29a517d5 + useColorScheme, // d585efdb usePressability, // b4e21b46 useWindowDimensions, // bb4b683f } diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js index dca2c6f8a08f..e1f8835be6da 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -17,11 +17,11 @@ export type ColorSchemeName = 'light' | 'dark'; export type ColorSchemeOverride = 'light' | 'dark' | 'unspecified'; export type AppearancePreferences = { - colorScheme?: ?ColorSchemeName, + colorScheme: ColorSchemeName, }; export interface Spec extends TurboModule { - +getColorScheme: () => ?ColorSchemeName; + +getColorScheme: () => ColorSchemeName; +setColorScheme: (colorScheme: ColorSchemeOverride) => void; // RCTEventEmitter diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 7fd44d84f22b..06f315af801f 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -24,7 +24,7 @@ function ColorSchemeSubscription() { useEffect(() => { const subscription = Appearance.addChangeListener( - ({colorScheme: newColorScheme}: {colorScheme: ?ColorSchemeName}) => { + ({colorScheme: newColorScheme}) => { setColorScheme(newColorScheme); }, ); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index 62d88ff75464..a32188996a13 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -2653,7 +2653,7 @@ protocol NativeAppStateSpec : public NSObjectRCTBridgeModule, public RCTTurboMod } protocol NativeAppearanceSpec : public NSObjectRCTBridgeModule, public RCTTurboModule { - public virtual NSString* _Nullable getColorScheme(); + public virtual NSString* getColorScheme(); public virtual void addListener:(NSString* eventName); public virtual void removeListeners:(double count); public virtual void setColorScheme:(NSString* colorScheme); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index b8feb8d7d212..d7818d3fe138 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -2653,7 +2653,7 @@ protocol NativeAppStateSpec : public NSObjectRCTBridgeModule, public RCTTurboMod } protocol NativeAppearanceSpec : public NSObjectRCTBridgeModule, public RCTTurboModule { - public virtual NSString* _Nullable getColorScheme(); + public virtual NSString* getColorScheme(); public virtual void addListener:(NSString* eventName); public virtual void removeListeners:(double count); public virtual void setColorScheme:(NSString* colorScheme); From 62a0ea0b14e9a38e527af626ba4daf552f42f386 Mon Sep 17 00:00:00 2001 From: Alex Hunt Date: Wed, 6 May 2026 03:09:11 -0700 Subject: [PATCH 3/3] Add 'auto' to ColorSchemeOverride, deprecate 'unspecified' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: NOTE: 👋🏻 **This is an RFC**, additional and separate to the previous 2 diffs. Looking for feedback :) Proposes an API tweak to `Appearance.setColorScheme()` to make it more idiomatic/understandable. - Aligns with the CSS `color-scheme` property vocabulary, where `auto` means "defer to the system preference". Replaces the ambiguous `unspecified` (now deprecated), which gave no indication of the resulting behaviour. See also: - History of this API + return type narrowing in D102527387. - Extended docs + diagram in https://github.com/facebook/react-native-website/pull/5060. **Alternative names considered** - `'reset'` - Implies reversing a change, not deferring to system - `'inherit'` - Weak — CSS inherit is element→parent, not app→OS Changelog: [General][Deprecated] - `Appearance.setColorScheme('unspecified')` is deprecated, use `'auto'` instead. Differential Revision: D103841988 --- .../react-native/Libraries/Utilities/Appearance.d.ts | 10 +++++++--- .../react-native/Libraries/Utilities/Appearance.js | 6 +++--- packages/react-native/React/Base/RCTConvert.mm | 1 + .../react/modules/appearance/AppearanceModule.kt | 1 + packages/react-native/ReactNativeApi.d.ts | 6 +++--- .../specs_DEPRECATED/modules/NativeAppearance.js | 2 +- .../js/examples/Appearance/AppearanceExample.js | 9 +++------ 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/Appearance.d.ts b/packages/react-native/Libraries/Utilities/Appearance.d.ts index c37a0ea577d2..153f0386fdf1 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.d.ts +++ b/packages/react-native/Libraries/Utilities/Appearance.d.ts @@ -11,7 +11,11 @@ import {NativeEventSubscription} from '../EventEmitter/RCTNativeAppEventEmitter' type ColorSchemeName = 'light' | 'dark'; -type ColorSchemeOverride = ColorSchemeName | 'unspecified'; +type ColorSchemeOverride = + | ColorSchemeName + | 'auto' + /** @deprecated Use 'auto' instead */ + | 'unspecified'; export namespace Appearance { type AppearancePreferences = { @@ -36,8 +40,8 @@ export namespace Appearance { /** * Force the application to always adopt a light or dark interface style. Pass - * `'unspecified'` to reset and follow the system default (removes any - * override). This does not affect the system UI, only the application. + * `'auto'` to reset and follow the system default (removes any override). + * This does not affect the system UI, only the application. */ export function setColorScheme(scheme: ColorSchemeOverride): void; diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index 4af8fe2e6b36..936b03f93852 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -107,8 +107,8 @@ export function getColorScheme(): ColorSchemeName | null { /** * Force the application to always adopt a light or dark interface style. Pass - * `'unspecified'` to reset and follow the system default (removes any - * override). This does not affect the system UI, only the application. + * `'auto'` to reset and follow the system default (removes any override). + * This does not affect the system UI, only the application. */ export function setColorScheme(colorScheme: ColorSchemeOverride): void { const state = getState(); @@ -117,7 +117,7 @@ export function setColorScheme(colorScheme: ColorSchemeOverride): void { NativeAppearance.setColorScheme(colorScheme); state.appearance = { colorScheme: - colorScheme === 'unspecified' + colorScheme === 'auto' || colorScheme === 'unspecified' ? NativeAppearance.getColorScheme() : colorScheme, }; diff --git a/packages/react-native/React/Base/RCTConvert.mm b/packages/react-native/React/Base/RCTConvert.mm index 0aacbd856107..9400163dd813 100644 --- a/packages/react-native/React/Base/RCTConvert.mm +++ b/packages/react-native/React/Base/RCTConvert.mm @@ -494,6 +494,7 @@ + (UIKeyboardType)UIKeyboardType:(id)json RCT_DYNAMIC RCT_ENUM_CONVERTER( UIUserInterfaceStyle, (@{ + @"auto" : @(UIUserInterfaceStyleUnspecified), @"unspecified" : @(UIUserInterfaceStyleUnspecified), @"light" : @(UIUserInterfaceStyleLight), @"dark" : @(UIUserInterfaceStyleDark), diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt index 65f1088ef51c..4028e2a9a20d 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/appearance/AppearanceModule.kt @@ -87,6 +87,7 @@ constructor( when (style) { "dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) "light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + "auto", "unspecified" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } diff --git a/packages/react-native/ReactNativeApi.d.ts b/packages/react-native/ReactNativeApi.d.ts index a2538b661fd0..6e7dd111ae46 100644 --- a/packages/react-native/ReactNativeApi.d.ts +++ b/packages/react-native/ReactNativeApi.d.ts @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<04955f1605996352fa3f3a3fc86ba937>> + * @generated SignedSource<> * * This file was generated by scripts/js-api/build-types/index.js. */ @@ -1859,7 +1859,7 @@ declare namespace CodegenTypes { } declare type ColorListenerCallback = (value: ColorValue) => unknown declare type ColorSchemeName = "dark" | "light" -declare type ColorSchemeOverride = "dark" | "light" | "unspecified" +declare type ColorSchemeOverride = "auto" | "dark" | "light" | "unspecified" declare type ColorValue = ____ColorValue_Internal declare type ComponentProvider = () => React.ComponentType declare type ComponentProviderInstrumentationHook = ( @@ -6073,7 +6073,7 @@ export { AppState, // 12012be5 AppStateEvent, // 80f034c3 AppStateStatus, // 447e5ef2 - Appearance, // 0e9bf4fe + Appearance, // 83e9641a AutoCapitalize, // c0e857a0 BackHandler, // f139fc69 BackPressEventName, // 4620fb76 diff --git a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js index e1f8835be6da..24293ff900ae 100644 --- a/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js +++ b/packages/react-native/src/private/specs_DEPRECATED/modules/NativeAppearance.js @@ -14,7 +14,7 @@ import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboMod export type ColorSchemeName = 'light' | 'dark'; -export type ColorSchemeOverride = 'light' | 'dark' | 'unspecified'; +export type ColorSchemeOverride = 'light' | 'dark' | 'auto' | 'unspecified'; export type AppearancePreferences = { colorScheme: ColorSchemeName, diff --git a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js index 06f315af801f..62bc5f9c2970 100644 --- a/packages/rn-tester/js/examples/Appearance/AppearanceExample.js +++ b/packages/rn-tester/js/examples/Appearance/AppearanceExample.js @@ -136,8 +136,8 @@ const ColorShowcase = (props: {themeName: string}) => ( const ToggleNativeAppearance = () => { const [nativeColorScheme, setNativeColorScheme] = useState< - ColorSchemeName | 'unspecified', - >('unspecified'); + ColorSchemeName | 'auto', + >('auto'); const colorScheme = useColorScheme(); useEffect(() => { @@ -156,10 +156,7 @@ const ToggleNativeAppearance = () => { title="Set to dark" onPress={() => setNativeColorScheme('dark')} /> -