microsoft / microsoft/fluentui-react-native
Theming: Better System Appearance Handling
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 179
- Avg merge
- 16h 17m
- Merged PRs (30d)
- 30
Description
Summary
Replace the current mix of requested appearance, resolved light or dark mode,
and high-contrast state with a structured model exposed through ThemeState.
Unify platform resolution so components can reliably ask which scheme is active
without re-reading theme context or depending on mutable module state.
The existing AppearanceOptions union combines several concepts, platform
helpers disagree on resolution, and ThemeState exposes only a
highContrast boolean.
Goal
Replace the ad hoc appearance and high-contrast handling with a structured
model: separate the requested color scheme from the resolved one, expose whether
the active theme is light or dark, and surface those values on ThemeState.
Stage
Stage 2 - Beta delivery.
Why it matters
- Observed. One union carries three different concepts.
AppearanceOptionsis'light' | 'dark' | 'darkElevated' | 'highContrast',
andTheme.host.appearanceisAppearanceOptions | 'dynamic'
(Theme.types.ts).
A theme in'highContrast'therefore has no representable light or dark
value, and a theme in'dynamic'has no resolved value at all. - Observed.
ThemeStatecannot answer "is this theme dark?".
useThemeState.ts
computeshighContrast: theme.host.appearance === 'highContrast' || isHighContrast(theme)and exposes no scheme value, so a component that needs
scheme-dependent behavior has to read the rawThemefrom context. - Observed. High contrast is determined differently on every platform:
constantfalsein
platformUtils.defaults.ts,
a module-level mutable flag set bysetIsHighContrastin
platformUtils.macos.ts,
atheme?.name === 'HighContrast'string check in
platformUtils.win32.ts,
andAppTheme.isHighContrastin
platformUtils.windows.ts. - Observed.
getCurrentAppearancealso diverges. The default implementation
returnsAppearance.getColorScheme() || fallbackfor'dynamic'; the Windows
implementation additionally returns'highContrast'when
AppTheme.isHighContrast, collapsing scheme and accessibility mode into one
return value. The default version treatsnullandundefinedinputs as the
fallback while the Windows version checks onlyundefined. - Inferred. Because the scheme is not resolved into
ThemeState, every
consumer that needs it re-derives it, and the macOS module-level mutable flag
means the value depends on call ordering rather than on the theme.
Observed current state
- Observed.
ThemeOptionshas bothappearance(including'dynamic') and
defaultAppearance, pluspaletteName
(Theme.types.ts). - Observed. The appearance helpers are exported publicly from the design
theming submodule
(theming/index.ts)
and re-exported by
theming-utils; they
were consolidated into design by
PR #4155. - Observed. Theme invalidation on appearance change is implemented per
platform theme rather than centrally:
createAppleTheme.macos.ts
registersAppearance.addChangeListenerand the macOShighContrastChanged
accessibility listener and callsThemeReference.invalidate(). - Observed.
useThemeStatecaches oneThemeStateperThemeobject
identity, so any appearance-derived value placed onThemeStateis only
recomputed when the theme object changes. - Observed. High-contrast token selection consumes the same union:
getAliasTokens(mode: AppearanceOptions)in
theme-tokens/src/getTokens.ts
branches on'light','dark' | 'darkElevated', and'highContrast'with
assertNever. - Observed. The Storybook app exposes exactly four fixed choices --
none, light, dark, high contrast -- built fromcreateDefaultTheme
(StorybookTheme.tsx); there
is no'dynamic'option.
Upstream appearance model (x3-design/fluent-design at d334acf)
- Observed. Upstream separates requested from resolved by construction. A
theme built bycreateThemealways carries both resolved modes
(ThemeResult.lightandThemeResult.dark), and selection happens at
consumption time through CSSlight-dark()withcolor-scheme, an explicit
[data-theme="light"|"dark"]selector, or aprefers-color-schememedia
query. Thedev/web/flex-themesREADME shows scoping a subtree by setting
color-schemeon an element. - Observed. The scheme is a two-value axis only: light and dark. There is no
darkElevatedequivalent. - Observed. Mode also parameterizes derived behavior rather than only token
values: the interaction algorithm's lightness direction and alpha constants are
per mode, andcontrast.mjstakes an explicitmodeargument with a
MODE_SURFACEconstant used to composite translucent colors. - Inferred. This supports modeling FURN's resolved scheme as a required,
always-concrete light or dark value onThemeState, with high contrast and any
elevated treatment as separate axes rather than as scheme members.
Scope
- Define the structured model: a requested color scheme (including a dynamic or
system option), a resolved scheme that is always concrete, and accessibility
modes such as high contrast and elevated dark treated as separate axes rather
than as scheme values. - Add the resolved values to
ThemeStatealongside the existinghighContrast
flag, keeping the identity and caching guarantees intact. - Unify the platform implementations behind one API, keeping platform detection
in the existing.macos.ts,.win32.ts, and.windows.tsfiles and removing
divergent behavior such as the'HighContrast'name check and the
null/undefinedinconsistency. - Define how a change in system appearance or high contrast invalidates the
theme and produces a newThemeState, and where that subscription belongs. - Provide a migration path for
AppearanceOptionsconsumers, including
getAliasTokensand the platform theme packages.
Out of scope
- Changing which token values each appearance resolves to; see
Default Values Codegen. - Adding a Flex-value context; see
Dynamic Theme Building. - Deriving interaction colors per scheme; see
Runtime Color Utilities. - Adding a dynamic appearance option to Storybook, which is listed as a
suggested addition.
Deliverables
- The structured appearance types, exported from the design theming submodule.
- Resolved scheme and accessibility values on
ThemeState. - One unified appearance API with consistent behavior across the default,
macOS, win32, and windows implementations. - A defined and implemented invalidation path for system appearance and
high-contrast changes. - A migration note plus updates for existing
AppearanceOptionsconsumers. - Tests covering resolution for each requested scheme on each platform
implementation, plusThemeStaterecomputation on change. - Changesets.
Acceptance criteria
- The requested scheme and the resolved scheme are separately representable,
and the resolved scheme is always a concrete light or dark value. - High contrast and any elevated dark treatment are represented
independently of the light/dark scheme. -
ThemeStateexposes the resolved scheme and the accessibility values, and
a component can determine whether the active theme is dark without reading
the rawTheme. - The default, macOS, win32, and windows implementations expose the same API
with documented per-platform sources, and no implementation infers high
contrast from a theme name string. -
getCurrentAppearancehandlesnullandundefinedidentically across
implementations. - A system appearance change or high-contrast change produces a new
ThemeState, covered by a test. - Existing consumers of
AppearanceOptions, including
getAliasTokens
and the platform theme packages, still compile and behave the same, or are
updated with a recorded migration. -
yarn build,yarn lage test, andyarn lage lintpass at the
repository root, and changesets are present.
Dependencies and ordering
- Depends on Dynamic Theme Building, which
determines howThemeStateis constructed and what a Flex-authored theme
supplies. - Pairs with Default Values Codegen: the
structured scheme model determines which generated appearance sets exist and
how one is selected. - Feeds Apple Theme, which relies on macOS appearance and
high-contrast resolution. - Feeds Runtime Color Utilities, whose
derivation direction depends on the resolved scheme.
Risks and open decisions
- Open decision. Whether
AppearanceOptionsis redefined in place or a new
type is added with the old one deprecated. Observed: it is exported
publicly from design theming and re-exported by thetheme-typesshim, and
getAliasTokensusesassertNeverover its members, so adding or removing a
member is a compile-time break there. - Open decision. Whether
'darkElevated'is a scheme, a modifier on dark, or
a platform-specific concept. Observed: it is currently a scheme value that
getAliasTokensfolds into the dark branch. - Open decision. Where appearance subscriptions live. Observed: they are
currently registered by individual platform themes such as
createAppleTheme.macos.ts, so a theme that does not register them never
updates. - Risk. The macOS module-level mutable
isHighContrastEnabledflag makes the
value global rather than per-theme; replacing it changes ordering behavior for
existing consumers that callsetIsHighContrastdirectly. - Risk.
ThemeStateis cached perThemeobject identity. If an appearance
change does not produce a new theme object, addedThemeStatevalues will go
stale; the invalidation path must be part of the change, not an assumption. - Risk. Platform detection must stay inside platform-suffixed files so the
React Native forks are not pulled into one type graph, per
AGENTS.md. - Risk, and a hard evidence gap. Observed: there is no high-contrast or
forced-colors theme anywhere in x3'sdev/web/flex-themes-- no such CSS file,
token set, or media query. FURN's high-contrast behavior therefore has no
upstream definition to align to and must be sourced from the platform APIs and
the Fluent token packages, which do ship high-contrast variants
(@fluentui-react-native/design-tokens-macoshclight/hcdarkand
@fluentui-react-native/design-tokens-win32hc). The owner should confirm
that split before the structured model is finalized.
Evidence and references
packages/agentic/design/src/theming/types/Theme.types.ts:AppearanceOptions,Theme.host.appearance,ThemeOptions.packages/agentic/design/src/theming/platformUtils.defaults.ts,platformUtils.macos.ts,platformUtils.win32.ts,platformUtils.windows.ts: the four divergent implementations.packages/agentic/design/src/useThemeState.ts: currentThemeStatefields and caching.packages/agentic/design/src/theming/themeReference.ts: invalidation mechanism.packages/theming/apple-theme/src/createAppleTheme.macos.ts: per-theme appearance subscriptions.packages/theming/theme-tokens/src/getTokens.ts:AppearanceOptionsconsumer withassertNever.apps/storybook/src/StorybookTheme.tsx: fixed appearance choices with no dynamic option.- microsoft/fluentui-react-native@
80bf14d: PR #4155, consolidated the appearance helpers into design. - microsoft/fluentui-react-native@
ea738f0: PR #4186,ThemeStateover existing FURN themes. - x3
dev/web/flex-themes/README.md:color-schemescoping, adaptive versus split files, and mode selectors. - x3
dev/web/flex-themes/createTheme.d.ts:ThemeResultcarrying both resolved modes andtoCssmode selection. - x3
dev/web/flex-themes/contrast.d.ts: explicitmodeparameter andMODE_SURFACE.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with Theme.types.ts, useThemeState.ts, the platformUtils.defaults.ts, platformUtils.macos.ts, platformUtils.win32.ts, and platformUtils.windows.ts implementations. Trace existing consumers in getTokens.ts and createAppleTheme.macos.ts, then run the repository build, test, and lint commands listed in the issue. Done means the structured API, invalidation path, migrations, platform resolution tests, ThemeState recomputation test, and changesets satisfy the acceptance criteria.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- design, frontend, mobile-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100