You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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. AppearanceOptions is 'light' | 'dark' | 'darkElevated' | 'highContrast',
and Theme.host.appearance is AppearanceOptions | '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.ThemeState cannot answer "is this theme dark?". useThemeState.ts
computes highContrast: theme.host.appearance === 'highContrast' || isHighContrast(theme) and exposes no scheme value, so a component that needs
scheme-dependent behavior has to read the raw Theme from context.
Observed.getCurrentAppearance also diverges. The default implementation
returns Appearance.getColorScheme() || fallback for 'dynamic'; the Windows
implementation additionally returns 'highContrast' when AppTheme.isHighContrast, collapsing scheme and accessibility mode into one
return value. The default version treats null and undefined inputs as the
fallback while the Windows version checks only undefined.
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.ThemeOptions has both appearance (including 'dynamic') and defaultAppearance, plus paletteName
(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
registers Appearance.addChangeListener and the macOS highContrastChanged
accessibility listener and calls ThemeReference.invalidate().
Observed.useThemeState caches one ThemeState per Theme object
identity, so any appearance-derived value placed on ThemeState is 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 from createDefaultTheme
(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 by createTheme always carries both resolved modes
(ThemeResult.light and ThemeResult.dark), and selection happens at
consumption time through CSS light-dark() with color-scheme, an explicit [data-theme="light"|"dark"] selector, or a prefers-color-scheme media
query. The dev/web/flex-themes README shows scoping a subtree by setting color-scheme on an element.
Observed. The scheme is a two-value axis only: light and dark. There is no darkElevated equivalent.
Observed. Mode also parameterizes derived behavior rather than only token
values: the interaction algorithm's lightness direction and alpha constants are
per mode, and contrast.mjs takes an explicit mode argument with a MODE_SURFACE constant used to composite translucent colors.
Inferred. This supports modeling FURN's resolved scheme as a required,
always-concrete light or dark value on ThemeState, 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 ThemeState alongside the existing highContrast
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.ts files and removing
divergent behavior such as the 'HighContrast' name check and the null/undefined inconsistency.
Define how a change in system appearance or high contrast invalidates the
theme and produces a new ThemeState, and where that subscription belongs.
Provide a migration path for AppearanceOptions consumers, including getAliasTokens and the platform theme packages.
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 AppearanceOptions consumers.
Tests covering resolution for each requested scheme on each platform
implementation, plus ThemeState recomputation 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.
ThemeState exposes the resolved scheme and the accessibility values, and
a component can determine whether the active theme is dark without reading
the raw Theme.
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.
getCurrentAppearance handles null and undefined identically 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, and yarn lage lint pass at the
repository root, and changesets are present.
Dependencies and ordering
Depends on Dynamic Theme Building, which
determines how ThemeState is 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.
Open decision. Whether AppearanceOptions is 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 the theme-types shim, and getAliasTokens uses assertNever over 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 getAliasTokens folds 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 isHighContrastEnabled flag makes the
value global rather than per-theme; replacing it changes ordering behavior for
existing consumers that call setIsHighContrast directly.
Risk.ThemeState is cached per Theme object identity. If an appearance
change does not produce a new theme object, added ThemeState values 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's dev/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/hcdark and @fluentui-react-native/design-tokens-win32hc). The owner should confirm
that split before the structured model is finalized.
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
AppearanceOptionsunion combines several concepts, platformhelpers disagree on resolution, and
ThemeStateexposes only ahighContrastboolean.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
AppearanceOptionsis'light' | 'dark' | 'darkElevated' | 'highContrast',and
Theme.host.appearanceisAppearanceOptions | 'dynamic'(
Theme.types.ts).A theme in
'highContrast'therefore has no representable light or darkvalue, and a theme in
'dynamic'has no resolved value at all.ThemeStatecannot answer "is this theme dark?".useThemeState.tscomputes
highContrast: theme.host.appearance === 'highContrast' || isHighContrast(theme)and exposes no scheme value, so a component that needsscheme-dependent behavior has to read the raw
Themefrom context.constant
falseinplatformUtils.defaults.ts,a module-level mutable flag set by
setIsHighContrastinplatformUtils.macos.ts,a
theme?.name === 'HighContrast'string check inplatformUtils.win32.ts,and
AppTheme.isHighContrastinplatformUtils.windows.ts.getCurrentAppearancealso diverges. The default implementationreturns
Appearance.getColorScheme() || fallbackfor'dynamic'; the Windowsimplementation additionally returns
'highContrast'whenAppTheme.isHighContrast, collapsing scheme and accessibility mode into onereturn value. The default version treats
nullandundefinedinputs as thefallback while the Windows version checks only
undefined.ThemeState, everyconsumer 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
ThemeOptionshas bothappearance(including'dynamic') anddefaultAppearance, pluspaletteName(
Theme.types.ts).theming submodule
(
theming/index.ts)and re-exported by
theming-utils; theywere consolidated into design by
PR #4155.
platform theme rather than centrally:
createAppleTheme.macos.tsregisters
Appearance.addChangeListenerand the macOShighContrastChangedaccessibility listener and calls
ThemeReference.invalidate().useThemeStatecaches oneThemeStateperThemeobjectidentity, so any appearance-derived value placed on
ThemeStateis onlyrecomputed when the theme object changes.
getAliasTokens(mode: AppearanceOptions)intheme-tokens/src/getTokens.tsbranches on
'light','dark' | 'darkElevated', and'highContrast'withassertNever.none, light, dark, high contrast -- built from
createDefaultTheme(
StorybookTheme.tsx); thereis no
'dynamic'option.Upstream appearance model (x3-design/fluent-design at
d334acf)theme built by
createThemealways carries both resolved modes(
ThemeResult.lightandThemeResult.dark), and selection happens atconsumption time through CSS
light-dark()withcolor-scheme, an explicit[data-theme="light"|"dark"]selector, or aprefers-color-schememediaquery. The
dev/web/flex-themesREADME shows scoping a subtree by settingcolor-schemeon an element.darkElevatedequivalent.values: the interaction algorithm's lightness direction and alpha constants are
per mode, and
contrast.mjstakes an explicitmodeargument with aMODE_SURFACEconstant used to composite translucent colors.always-concrete light or dark value on
ThemeState, with high contrast and anyelevated treatment as separate axes rather than as scheme members.
Scope
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.
ThemeStatealongside the existinghighContrastflag, keeping the identity and caching guarantees intact.
in the existing
.macos.ts,.win32.ts, and.windows.tsfiles and removingdivergent behavior such as the
'HighContrast'name check and thenull/undefinedinconsistency.theme and produces a new
ThemeState, and where that subscription belongs.AppearanceOptionsconsumers, includinggetAliasTokensand the platform theme packages.Out of scope
Default Values Codegen.
Dynamic Theme Building.
Runtime Color Utilities.
suggested addition.
Deliverables
ThemeState.macOS, win32, and windows implementations.
high-contrast changes.
AppearanceOptionsconsumers.implementation, plus
ThemeStaterecomputation on change.Acceptance criteria
and the resolved scheme is always a concrete light or dark value.
independently of the light/dark scheme.
ThemeStateexposes the resolved scheme and the accessibility values, anda component can determine whether the active theme is dark without reading
the raw
Theme.with documented per-platform sources, and no implementation infers high
contrast from a theme name string.
getCurrentAppearancehandlesnullandundefinedidentically acrossimplementations.
ThemeState, covered by a test.AppearanceOptions, includinggetAliasTokensand 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 therepository root, and changesets are present.
Dependencies and ordering
determines how
ThemeStateis constructed and what a Flex-authored themesupplies.
structured scheme model determines which generated appearance sets exist and
how one is selected.
high-contrast resolution.
derivation direction depends on the resolved scheme.
Risks and open decisions
AppearanceOptionsis redefined in place or a newtype is added with the old one deprecated. Observed: it is exported
publicly from design theming and re-exported by the
theme-typesshim, andgetAliasTokensusesassertNeverover its members, so adding or removing amember is a compile-time break there.
'darkElevated'is a scheme, a modifier on dark, ora platform-specific concept. Observed: it is currently a scheme value that
getAliasTokensfolds into the dark branch.currently registered by individual platform themes such as
createAppleTheme.macos.ts, so a theme that does not register them neverupdates.
isHighContrastEnabledflag makes thevalue global rather than per-theme; replacing it changes ordering behavior for
existing consumers that call
setIsHighContrastdirectly.ThemeStateis cached perThemeobject identity. If an appearancechange does not produce a new theme object, added
ThemeStatevalues will gostale; the invalidation path must be part of the change, not an assumption.
React Native forks are not pulled into one type graph, per
AGENTS.md.forced-colors theme anywhere in x3's
dev/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 confirmthat 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.80bf14d: PR Consolidate getCurrentAppearance, isHighContrast and setHighContrast in design package #4155, consolidated the appearance helpers into design.ea738f0: PR Ensure that useThemeState will work with existing furn themes #4186,ThemeStateover existing FURN themes.dev/web/flex-themes/README.md:color-schemescoping, adaptive versus split files, and mode selectors.dev/web/flex-themes/createTheme.d.ts:ThemeResultcarrying both resolved modes andtoCssmode selection.dev/web/flex-themes/contrast.d.ts: explicitmodeparameter andMODE_SURFACE.