Skip to content

WinUI reference: For the full property surface and design guidance, see Colors.

A Microsoft.UI.Reactor (Reactor) theme token is a name for a color that the system resolves at render time. The same token (Theme.Accent) emits a different Brush depending on whether the app is in light mode, dark mode, or high contrast — and switches without a re-render when the user flips themes. The alternative — a literal like "#0066CC" baked into a modifier — locks the value to a single visual mode and silently breaks dark theme the moment a user opens settings. Tokens exist so the styling layer can hand a brush to a control without caring which theme is active. The most common mistake is reaching for a literal because "I'll just hardcode this one"; the REACTOR_THEME_001 analyzer catches that at build time so it cannot ship.

Theming Tokens

Reactor exposes 35 named theme tokens on the static Theme class plus a Theme.Ref(string) escape hatch for any XAML resource key. Tokens resolve through WinUI's resource system (XamlControlsResources + the app's ThemeDictionaries), so the values they produce match the design system Microsoft ships in WinUI Fluent colors.

Lead snippet

class SwatchGrid : Component
{
    public override Element Render() => ScrollView(
        VStack(16,
            Heading("Theme tokens"),
            SwatchSection("Accent", new[] {
                ("Accent", Theme.Accent),
                ("AccentSecondary", Theme.AccentSecondary),
                ("AccentTertiary", Theme.AccentTertiary),
                ("AccentDisabled", Theme.AccentDisabled),
            }),
            SwatchSection("Text", new[] {
                ("PrimaryText", Theme.PrimaryText),
                ("SecondaryText", Theme.SecondaryText),
                ("TertiaryText", Theme.TertiaryText),
                ("DisabledText", Theme.DisabledText),
                ("AccentText", Theme.AccentText),
            }),
            SwatchSection("Surfaces", new[] {
                ("SolidBackground", Theme.SolidBackground),
                ("CardBackground", Theme.CardBackground),
                ("SmokeFill", Theme.SmokeFill),
                ("SubtleFill", Theme.SubtleFill),
                ("LayerFill", Theme.LayerFill),
            }),
            SwatchSection("Control fill", new[] {
                ("ControlFill", Theme.ControlFill),
                ("ControlFillSecondary", Theme.ControlFillSecondary),
                ("ControlFillTertiary", Theme.ControlFillTertiary),
                ("ControlFillDisabled", Theme.ControlFillDisabled),
                ("ControlFillInputActive", Theme.ControlFillInputActive),
            }),
            SwatchSection("Stroke", new[] {
                ("CardStroke", Theme.CardStroke),
                ("SurfaceStroke", Theme.SurfaceStroke),
                ("DividerStroke", Theme.DividerStroke),
                ("ControlStroke", Theme.ControlStroke),
                ("ControlStrokeSecondary", Theme.ControlStrokeSecondary),
            }),
            SwatchSection("Signal", new[] {
                ("SystemAttention", Theme.SystemAttention),
                ("SystemSuccess", Theme.SystemSuccess),
                ("SystemCaution", Theme.SystemCaution),
                ("SystemCritical", Theme.SystemCritical),
                ("SystemNeutral", Theme.SystemNeutral),
                ("SystemSolidNeutral", Theme.SystemSolidNeutral),
            })
        ).Padding(20)
    );

    private static Element SwatchSection(string title, (string Name, ThemeRef Ref)[] tokens) =>
        VStack(8,
            SubHeading(title),
            VStack(4, tokens.Select(t => Row(t.Name, t.Ref)).ToArray())
        );

    private static Element Row(string name, ThemeRef token) => HStack(12,
        new BorderElement(Empty())
            .Background(token)
            .Size(40, 24)
            .WithBorder("#DDDDDD"),
        TextBlock(name).Width(220),
        TextBlock(token.ResourceKey).Opacity(0.6)
    );
}

Token swatches — light theme Token swatches — dark theme

The same grid, both themes. Every cell is the result of ThemeRef resolution against the active ThemeDictionary; switching themes is a property update on the host, not a re-render of the component.

Caveat: A hardcoded color string in .Background("#0066CC") skips the theme swap entirely. The button stays blue when the user flips to dark mode, and white text on a now-still-blue background still satisfies WCAG against light's neutral — but the dark-mode neutral behind it makes the page look broken. Use Theme.Accent (or the matching Theme.Ref(key)) on .Background / .Foreground / .WithBorder; REACTOR_THEME_001 flags the literal at build time so the regression never reaches review.

How resolution works

ThemeRef is a readonly record struct holding the resource key. When a modifier like .Background(token) reaches the reconciler, the framework walks from the materialized FrameworkElement up the visual tree looking for the nearest RequestedTheme, falls back to ActualTheme, then Application.RequestedTheme. The brush for the chosen theme name comes from Application.Resources.ThemeDictionaries[name] (or the nearest merged dictionary).

class ThemeRefGood : Component
{
    public override Element Render() =>
        Button("Click me", () => { }).Background(Theme.Accent);
}

When the user flips the system theme, WinUI raises ActualThemeChanged; Reactor subscribes and updates the brush on every element that referenced a ThemeRef. Components that branch on the scheme use UseColorScheme:

// UseColorScheme reads the current scheme (Light / Dark) reactively so a
// component can branch on the value without re-implementing the resolver.
class SchemeAwareBadge : Component
{
    public override Element Render()
    {
        var scheme = UseColorScheme();
        var label = scheme == ColorScheme.Dark ? "Dark mode" : "Light mode";
        return TextBlock(label).Foreground(Theme.PrimaryText);
    }
}

UseColorScheme() returns the current ColorScheme (Light / Dark / HighContrast) and re-runs render when the value changes. Reach for it only when the branch can't be expressed as a token (e.g. a non-color decision like which icon to render).

Token catalog

35 named tokens across 6 groups, plus the Theme.Ref(string) escape hatch.

Accent (4 tokens)

Token WinUI key Where to use
Theme.Accent AccentFillColorDefaultBrush Primary action button background.
Theme.AccentSecondary AccentFillColorSecondaryBrush Pressed / hover accent variants.
Theme.AccentTertiary AccentFillColorTertiaryBrush Subtle accent surfaces.
Theme.AccentDisabled AccentFillColorDisabledBrush Disabled state on an accent control.

Text (5 tokens)

Token WinUI key Where to use
Theme.PrimaryText TextFillColorPrimaryBrush Headlines and body.
Theme.SecondaryText TextFillColorSecondaryBrush Supporting copy.
Theme.TertiaryText TextFillColorTertiaryBrush Captions and timestamps.
Theme.DisabledText TextFillColorDisabledBrush Disabled labels.
Theme.AccentText AccentTextFillColorPrimaryBrush Inline accent text (links, badges).

Surfaces (5 tokens)

Token WinUI key Where to use
Theme.SolidBackground SolidBackgroundFillColorBaseBrush Page background.
Theme.CardBackground CardBackgroundFillColorDefaultBrush Card / panel container.
Theme.SmokeFill SmokeFillColorDefaultBrush Modal scrim.
Theme.SubtleFill SubtleFillColorSecondaryBrush Hover wash for list rows.
Theme.LayerFill LayerFillColorDefaultBrush Stacked-card layer.

Control fill (5 tokens)

Token WinUI key Where to use
Theme.ControlFill ControlFillColorDefaultBrush Default neutral control surface.
Theme.ControlFillSecondary ControlFillColorSecondaryBrush Hover state.
Theme.ControlFillTertiary ControlFillColorTertiaryBrush Pressed state.
Theme.ControlFillDisabled ControlFillColorDisabledBrush Disabled.
Theme.ControlFillInputActive ControlFillColorInputActiveBrush Active TextBox / NumberBox fill.

Stroke (5 tokens)

Token WinUI key Where to use
Theme.CardStroke CardStrokeColorDefaultBrush Card outline.
Theme.SurfaceStroke SurfaceStrokeColorDefaultBrush Page-level dividers.
Theme.DividerStroke DividerStrokeColorDefaultBrush Inline list separators.
Theme.ControlStroke ControlStrokeColorDefaultBrush Default control border.
Theme.ControlStrokeSecondary ControlStrokeColorSecondaryBrush Focus / pressed border.

Signal (11 tokens)

Token WinUI key Where to use
Theme.SystemAttention SystemFillColorAttentionBrush Attention call-out.
Theme.SystemSuccess SystemFillColorSuccessBrush Success state.
Theme.SystemCaution SystemFillColorCautionBrush Warning state.
Theme.SystemCritical SystemFillColorCriticalBrush Error state.
Theme.SystemNeutral SystemFillColorNeutralBrush Info / neutral.
Theme.SystemSolidNeutral SystemFillColorSolidNeutralBrush Solid neutral surface.
Theme.SystemAttentionBackground SystemFillColorAttentionBackgroundBrush Attention banner background.
Theme.SystemSuccessBackground SystemFillColorSuccessBackgroundBrush Success banner background.
Theme.SystemCautionBackground SystemFillColorCautionBackgroundBrush Warning banner background.
Theme.SystemCriticalBackground SystemFillColorCriticalBackgroundBrush Error banner background.
Theme.SystemNeutralBackground SystemFillColorNeutralBackgroundBrush Info banner background.

(Theme.SystemSolidAttention rounds it to 35 + the Theme.Ref(key) escape hatch — total surface ≥ 37 per spec §5.3.)

Custom keys

// Reference any XAML resource by string key — covers app-level overrides
// and any token Reactor doesn't surface as a typed accessor.
Button("Custom", () => { })
    .Background(Theme.Ref("MyAppTitleBarBackground"));

Patterns

Theming an InfoBar by severity

The signal-background tokens pair with the foreground signal tokens to produce inline status banners that read correctly in either theme.

Element StatusBanner(string text, Severity severity) => HStack(8,
    TextBlock(text).Foreground(severity switch {
        Severity.Success  => Theme.SystemSuccess,
        Severity.Warning  => Theme.SystemCaution,
        Severity.Critical => Theme.SystemCritical,
        _                  => Theme.SystemNeutral,
    })
).Background(severity switch {
    Severity.Success  => Theme.SystemSuccessBackground,
    Severity.Warning  => Theme.SystemCautionBackground,
    Severity.Critical => Theme.SystemCriticalBackground,
    _                  => Theme.SystemNeutralBackground,
}).Padding(12).CornerRadius(4);

Pair this with commanding to render the same banner on success / failure of a Command<T>.

App-level token override

When the design system needs a brand color not in the WinUI palette, define it once in Application.Resources (or a merged dictionary) and reach for it through Theme.Ref:

<!-- App.xaml -->
<Application.Resources>
  <ResourceDictionary>
    <ResourceDictionary.ThemeDictionaries>
      <ResourceDictionary x:Key="Light">
        <SolidColorBrush x:Key="BrandPrimaryBrush" Color="#005FB8" />
      </ResourceDictionary>
      <ResourceDictionary x:Key="Dark">
        <SolidColorBrush x:Key="BrandPrimaryBrush" Color="#60AAFA" />
      </ResourceDictionary>
    </ResourceDictionary.ThemeDictionaries>
  </ResourceDictionary>
</Application.Resources>
Button("Buy now", BuyAction)
    .Background(Theme.Ref("BrandPrimaryBrush"));

The brand color participates in theme swap because the override has a light and dark variant. Defining it without ThemeDictionaries freezes it to one theme — exactly the trap Theme.Ref is built to prevent.

Per-element theme override

A single sub-tree can opt out of the app theme via RequestedTheme — useful for a video player surface or a print-preview pane that should always look light:

ScrollView(content).RequestedTheme(ElementTheme.Light);

The walk in Theme.Resolve picks up the local override first, so every ThemeRef inside that sub-tree resolves against the light dictionary even when the rest of the app is dark.

Common Mistakes

Hardcoded color literal

// Don't:
Button("Save", () => { }).Background("#0066CC");
//                                    ^^^^^^^^^^
// REACTOR_THEME_001 — hardcoded color literal on a theme-aware modifier.
// The button stays blue when the user flips to dark mode.
class ThemeRefGood : Component
{
    public override Element Render() =>
        Button("Click me", () => { }).Background(Theme.Accent);
}

The literal locks the value to one theme; the token participates in the swap. The analyzer surfaces this at build time so a regression can't ship; treat the warning as an error in CI.

Light-only test missing the dark regression

A common review mistake: the screenshot in the PR is light-mode, the reviewer signs off, and the dark-mode breakage lands on main. The testing page covers the fixture pattern that mounts the same component twice — once with RequestedTheme(Light), once with (Dark) — and snapshots both.

[Theory]
[InlineData(ElementTheme.Light)]
[InlineData(ElementTheme.Dark)]
public void StatusBanner_renders_in_both_themes(ElementTheme theme)
{
    var rendered = Mount(new StatusBanner("Saved", Severity.Success));
    rendered = rendered.WithModifier(m => m.RequestedTheme(theme));
    Assert.Empty(AccessibilityScanner.Scan(rendered));
}

Resolving a non-themed key as a ThemeRef

Theme.Ref("MyAppHeaderBackground") against a resource that exists only in Application.Resources (not in a ThemeDictionaries entry) returns a single brush regardless of theme. The page renders, but the color doesn't swap. If the goal is theme-aware, the resource must live in ThemeDictionaries; otherwise hardcode the color in app code and mark it intentional with a comment.

Tips

Reach for a typed token first, the Theme.Ref escape hatch second. Typed tokens land with IntelliSense and a doc comment; the string keys do not. If a color is missing from the typed surface and it's a stable WinUI brush, add it to Theme.cs rather than proliferating Theme.Ref("…") calls.

Treat REACTOR_THEME_001 as an error in CI. The cost of fixing a hardcoded color is one line; the cost of a dark-theme regression landing on main is a customer report.

Don't burn cycles re-resolving on every render. ThemeRef is a record struct — a stack allocation. The resolver is called once per modifier application; UseColorScheme only re-runs render when the scheme actually changes.

Reference

Concept API Notes
Token Theme.<Name> 35 typed accessors in src/Reactor/Core/Theme.cs.
Escape hatch Theme.Ref(key) Any XAML resource key, including app-level overrides.
Reactive read UseColorScheme() Returns ColorScheme; triggers re-render on swap.
Per-element override .RequestedTheme(ElementTheme) Walks visual tree, resolves nearest override.
Analyzer REACTOR_THEME_001 Flags hardcoded literals on theme-aware modifiers.
WinUI surface XamlControlsResources Owning ThemeDictionary for every named token.

Next Steps

  • Styling — Previous in the styling chain: the modifier surface that consumes ThemeRef values.
  • Animation — Next: animating between themed values; how the brush transitions through a theme swap.
  • Accessibility — Contrast budgets that determine which token to reach for in each surface.
  • Devtools Internals — How the dev-menu's theme-swap toggle drives the same resolver this page documents.
  • Rules of Reactor — Where the REACTOR_THEME_001 analyzer is listed alongside the rest of the enforced idioms.